Something in a tool’s tree went wrong on a real call. Two questions follow, and this page answers both: what did the caller experience, and where do you go to find out what happened. The mechanics of each operation are elsewhere — the operation tree, http, code. This page is about the failure path through them.

What the caller hears

When an operation fails and on_error is abort, the tool stops and the model is handed one line as the tool’s result, marked as an error:
That string never reaches the caller. It reaches the model, as the tool’s output, and the model then writes the next thing the agent says. So what the caller actually hears is decided by the agent’s prompt, not by us — and a model with no instruction for this case improvises. That is the whole problem. A model improvising about a failed booking is how a caller gets told something untrue: “You’re all set, I’ve got you down for Thursday” is a plausible next sentence after a tool call the model was told almost nothing about. Write the failure case into the prompt. One or two lines per tool that matters:
Two things make those lines work: they say what the agent must not claim, and they give it somewhere to go. See prompting for the rest of the structure. Three different strings can arrive as a tool’s error output, and on a transcript they mean different things:
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.
If the lookup fails, 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.
A continued failure leaves no record on a live call. It is logged on our side and nowhere yours: no timeline event, no tool.failed webhook, and the tool call is recorded as having succeeded, because from the model’s point of view it did. A test run is the only place you can see one, where the step goes red and the rest of the trace continues past it. Use continue deliberately, on operations you have decided you do not need to hear about.

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 the balance lookup fails, the 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 with HTTP 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

A 3xx 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 own timeout 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 path whose value is null fails the whole operation, indistinguishably from a wrong path. The message is the same. So an endpoint that answers {"booking": {"reference": null}} on a booking it did not make kills the tool with an error that reads like a typo.Do not publish a field that can be null. Return a placeholder instead — an empty string, a sentinel like "unknown" — either from your own API or from a code operation that maps nulls before anything publishes.

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, with destination 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

A long_running_task tool reports success the moment it is dispatched. The model is handed “Sure — let me take care of that. One moment.”, the agent keeps talking, and the tree runs on detached. If it then fails, the model is never told: it has already moved on, and the caller has already been told the work is under way.So a call can end with a caller who believes something happened that did not. Nothing in the conversation will say otherwise.The failure is recorded, in three places. On the call detail, the tool’s card turns red and carries the line “This tool runs in the background, so the agent was told it had started and carried on. It never learned about this failure.” — its duration still measures the dispatch, not the work, because the detached work has no end anyone timed. On the call’s timeline it lands as a tool.failed_detached event carrying the error. And a tool.failed webhook fires, which is the only one of the three that reaches you without somebody opening a page.If a long-running tool’s outcome matters to the caller, do not use the flag. Take the wait, or say something before it with a say operation.

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.