What the caller hears
When an operation fails andon_error is abort, the tool stops and the model
is handed one line as the tool’s result, marked as an error:
A tool that hangs up or hands off tells the model nothing at all, whether it
failed or not: there is nobody left to narrate to. See
derived silence.
on_error in practice
on_error sits on every operation, and it has two values.
Reach for
continue when the operation is not the point of the call, and leave
it at abort when it is. The test is simple: if the caller would rather have a
partial answer than an apology, continue; if a wrong answer is worse than no
answer, abort.
A guard that tolerates a failure, and one that must not
Both are in this tree. The enrichment lookup is a nice-to-have — if the CRM is down the caller still gets booked. The booking itself is the point, so it keeps the default.userdata.tier is never written and the tree carries on. If
the booking fails, the say never runs, the model gets the apology line, and
the prompt decides what the caller hears.
continue on an if ends the tree
An if is terminal in its chain, so there is no next operation to carry on to.
on_error: "continue" on one means neither branch runs and the tree ends
there — which is a real outcome, and sometimes the one you want.
It matters most where the condition can genuinely fail to evaluate. gt and
lt need two numbers, and an upstream operation that was allowed to continue
publishes nothing, so the left side resolves to empty and the comparison raises
rather than quietly taking else:
if cannot compare an empty value against 0,
and the tool ends there — rather than taking else and telling somebody their
account is paid up when nobody knows. That is the trade this pattern makes, and
it is worth making deliberately, because the cost is real: every operation in
this tree either speaks for itself or is marked silent, so the tool is
silent by derivation, and a run where
neither say fired ends the turn having said nothing at all. The caller hears
a pause.
Change that on_error to abort and the caller gets the apology instead. Both
are defensible; the wrong answer is not. See
conditionals for how the comparisons work.
Every way an operation fails
A status of 400 or above
The most common one. The operation fails withHTTP request failed with status 500,
and the response body your server sent is kept beside it in a test run’s trace,
because “your server said 500” is half an answer.
Nothing is retried. One request, one outcome — if a flaky endpoint should not
take the call down with it, that is what on_error: "continue" is for.
A redirect
A3xx is not a failure, and it is not followed. The tree gets whatever
body the redirect carried, which is usually nothing — so the operation succeeds
and its result is {}.
This is the confusing one, because it breaks one step later and blames the wrong
thing: any publish_fields on that operation then fails with
publish field path '…' was not found in operation output, which reads like a
wrong path. If a path you are certain about stops resolving, check whether the
URL redirects — a host that 301s to its canonical form (a missing trailing
slash, http to https, an apex to www) does exactly this. Point the URL at
the endpoint itself.
A redirect that does carry a body, such as the HTML stub some servers send,
fails as a non-JSON response instead. Same cause, different message.
A body that is not JSON
HTTP operation returned a non-JSON response. Usually an HTML error page from a
proxy, or a login page from an endpoint that did not recognise the credential
and answered 200 anyway. The decoded body is in the trace.
An empty body is not this: it succeeds and the result is {}, with the same
downstream consequence as a redirect. A body over a megabyte fails with
HTTP response exceeds 1048576 bytes.
A timeout
The operation’s owntimeout elapsed — 20 seconds by default on an http, 10
on a code, 5 on a frontend_rpc. All of them are in seconds, and all of
them are a caller sitting in silence, so the fix is rarely a longer timeout. If
the endpoint is genuinely slow, use the tool-level long_running_task flag and
read the trap below.
A test run has a second, harder ceiling: the whole run is capped at 60 seconds
and fails with the run exceeded the 60s limit for a test.
A publish_fields path that does not resolve
publish field path 'booking.reference' was not found in operation output. The
operation itself worked; reading a value out of it did not. Usually a path one
level too deep or one level too shallow — the published list on each step of a
test run is the fastest way to see what actually landed.
A code operation that throws
Anything the TypeScript throws, plus running out of time or memory, fails the
operation as code operation failed: <the message>. When the script logged
anything, the last ten console lines are appended: (console: …). A timeout
reads execution exceeded 10000ms — the number is the operation’s timeout in
milliseconds, which is the one place milliseconds appear.
A script that will not compile never gets this far. TypeScript is compiled at
publish, so a compile error is a publish error naming the line.
A blocked URL
Every address a host resolves to must be public. A private, loopback, link-local, carrier-grade-NAT, unique-local, multicast or reserved address is refused before the request goes out, withdestination 169.254.169.254 is not allowed (private/internal). A host that
does not resolve fails with could not resolve host 'api.example.com', and a
scheme that is not http or https with
HTTP operation URL must be http(s) with a host.
None of these are transient, and none of them will start working on their own.
The full rules are in the HTTP operation; the
practical consequence is that an endpoint on your private network is not
reachable from a tool.
A missing secret
secret 'CRM_API_KEY' does not exist, or secrets do not exist: A, B when more
than one is gone. This one fails before any operation runs: the secrets a
tree references are gathered up front, so a tree missing one never opens a
socket. It happens when a secret is deleted or renamed after the tool was
published — the published version still references the old name. See
secrets.
Because it happens outside the tree walk, this failure behaves differently on a
live call from every other one on this page: the model is handed
An internal error occurred rather than the apology line, no tool.failed
webhook fires, and on_error has no say in it — there is no operation for the
setting to sit on. A test run names it precisely, as error_type: tool_auth.
The long-running trap
Where to look, in order
1
The call detail's transcript
Every tool call is in there beside the speech, with the arguments the model
chose and what came back. A failed call is marked failed and shows the
failure text; when the model was told something different from what
happened, the card shows both — “The model was told” is the line that
explains why the agent then said something odd. A required argument the
model left empty gets its own badge, which is worth checking first: a tool
that “ran fine” on an empty required field is the most common reason a call
reads as a failure. See calls.
2
The timeline events
events beside the transcript is the durable runtime trace. tool.started
and tool.ended carry each call’s timing and outcome, tool.failed_detached
is the only record of the trap above, and tool.step_limit_reached says the
agent wanted more tools in one turn than it was allowed and was made to
answer without any — which reads as an ordinary turn everywhere else. The
dashboard filters the list to tool. events in one click.3
The tool test panel
Once you know which operation broke, reproduce it.
POST /v1/tools/{tool_id}/run
runs the draft against your real endpoints and reports every step: the
resolved request, the response, what each operation published, and where it
stopped. error_type names whose problem it is — tool_config,
tool_auth, endpoint_error, endpoint_unreachable, timeout,
code_error, platform or unknown — and the classification is structural
rather than string matching, so an HTTP step that recorded no request had a
bad config, one with a request and no response never reached you, and one
with a response means you answered. The full table, with what each one sends
you to fix, is in testing and publishing.A test run executes for real. A tool that books a slot books a slot.4
The webhooks
tool.invoked fires when the model calls a tool, carrying the tool name and
the arguments; tool.failed fires when an operation aborts the tool,
carrying the error. Together they are how a failure reaches you without
anyone watching a dashboard, and tool.failed is the only surface that
reports a detached long-running failure without somebody opening a page.Three gaps worth knowing, all of them cases where neither event fires: a
failure handled by on_error: "continue", a tool call rejected because the
arguments did not match the schema, and a missing secret — the last two
because the tree never started. See webhooks.A
code operation’s console output is not visible on a live call. The
lines are captured and sent to the platform’s own logs, and no page in the
dashboard carries them. The one exception is a failure: the last ten lines are
appended to the error text, so they reach the trace and the tool.failed
webhook that way.On a test run they are shown in full
beside the operation’s return value. Debug a script there.Symptom to cause
Next
Debugging agents
The wider playbook: slow, odd, hung up, never answered.
Testing and publishing
Reproduce the failure against the draft and read every step.
Calls
The call record, the transcript and the timeline in full.
Patterns
Guard-and-continue and escalation, as complete trees.