Building a server for Studio
Your agents reach your own systems through your server. Studio sends it two kinds of request:
This page is for any server that answers Studio’s tool requests: to look things up for the agent, to take actions, or to start and resolve situations. MCP servers work differently, and aren’t covered here; an MCP tool can still start and resolve situations, as Starting situations from your server explains.
A complete example in TypeScript, with tests, is at jambonz/agent-studio-situations-example.
What your server needs
- A public
httpsURL on a public domain name, with a valid certificate. Studio doesn’t send tool requests to plainhttp, to IP addresses in private ranges, or to names that resolve to them. - An answer within the tool’s timeout: 1 to 15 seconds, 8 by default. The caller waits in silence while it runs, so aim for well under a second.
- No redirects. Studio doesn’t follow them; a
3xxanswer is a failed request.
For a test against a server on your own machine, a tunnel such as ngrok gives you a public https URL.
The request
Each HTTP tool is one request: its method and URL, with the arguments the model fills in.
Arguments
A {name} in the tool’s URL is replaced with that argument, URL-encoded. The other arguments are sent according to the
tool’s Send arguments as setting:
A GET request always sends them in the query string, and has no body.
Argument names reach your server exactly as the tool’s parameters name them.
Headers
Use X-Studio-Call-Sid to tie requests from the same call together, and to find the call in
Calls.
Verifying requests
Studio signs every tool request. Check the signature before you act on a request: without it, anyone who finds your URL can call your tool.
Reject the request when v1 doesn’t match, or when t is more than 300 seconds from your server’s clock. Compare
signatures in constant time.
The signing secret
Each HTTP tool has its own signing secret. Studio creates it when you first save the tool and shows it once. Rotate secret… on the tool replaces it: requests are signed with the new secret from then on, so give your server the new one straight away.
Keep each tool’s secret in your server’s configuration or secret store, never in code, and look it up by tool:
X-Studio-Tool, or the path you served the tool on.
Node.js
With Express, read the raw body with express.raw({ type: '*/*' }) (or keep it with the verify option of
express.json()), and use req.originalUrl for the path and query.
Python
Read the body with your framework’s raw-body call, such as request.get_data() in Flask or await request.body() in
FastAPI. Take the path and query from the request line as received: some frameworks decode the path, which breaks the
signature for any path with an encoded character. Under Gunicorn, the WSGI environ’s RAW_URI has it unchanged.
Common mistakes
- Verifying the parsed body. Re-serialised JSON rarely matches byte for byte. Verify the raw body, then parse it.
- Using the full URL. Sign only the path and query string, as received, without the scheme and host.
- Rebuilding the query string. Use it as it arrived; reordering or re-encoding it changes the signature.
- Using the result webhook’s formula. It signs a different string: see below.
The result webhook’s signature
The result webhook is signed in the same header with the same t and v1 parts, but v1
covers only the timestamp and the body, and the agent’s webhook signing secret (starting whsec_) is the key:
The response
Answer with a 2xx status and a JSON object. What happens next depends on the status:
Only GET requests are retried, up to twice, and only within the tool’s timeout, so a POST never reaches your server
twice. Make a GET safe to repeat.
When a request fails, the tool’s If the request fails setting decides what the caller hears: the model is told it failed and recovers, or the agent says a fixed line.
What the model sees
The tool’s Response settings shape the result before the model reads it:
- The full body, or only the value at a Field path such as
order.statusoritems[0].name; - cut to Max characters, 2,000 by default. Longer results slow the agent and use tokens.
Return what the model needs to answer the caller, in plain field names, and nothing else. A field like
"next_step": "Ask whether they want it delivered or collected." works better than a code the model has to interpret.
Never return data the caller shouldn’t hear: the model may read out anything in the result.
Changing situations
A result can start or resolve a situation with an actions object:
Studio applies it and removes it before the model sees the result, even with a Field path or when the result is cut. See Starting situations from your server.
Testing
- Test this tool, on the tool, sends a real, signed request from Studio’s servers with sample values, and shows the request, your response and what the model would receive. It tests the saved tool, so save first.
- A test call shows each tool call on its turn, with its arguments, result and round-trip time.
- Log each request’s tool,
X-Studio-Call-Sid, status and timing. Logging whole bodies helps while testing, but they hold what callers said, so turn it off for real calls.
Checklist
- public
httpsURL, no redirects - answers within the tool’s timeout, ideally under a second
- verifies
X-Studio-Signatureover the raw body, with each tool’s own secret - rejects signatures more than 300 seconds old
- returns a JSON object with only what the model needs
-
GETtools are safe to repeat - logs the call id with each request, and no caller data in production
Next
- Tools: adding HTTP tools to an agent
- Starting situations from your server
- Result webhook: the payload Studio posts after each call