operations is an ordered list. It runs top to bottom when the model calls the tool, and it is where everything the tool actually does lives. Sending operations replaces the tree entirely. There is no partial update of a tree — PATCH /v1/tools/{tool_id} leaves out fields you omit, but a tree you do send is the whole tree. Read the current one, change it, send it back.

One operation

Every operation is {kind, config, on_error?}, and kind decides the rest. An operation is a union of eleven shapes, one per kind, each carrying only its own fields — so a field belonging to another kind is rejected, not ignored. A publish_fields on a say is refused at save with 422 operations.0.say.publish_fields: Extra inputs are not permitted, and a then on an http the same way.
config is required on every kind but end_call, which has no settings.

The eleven kinds

Which fields exist on which kinds

silent, publish_fields and background_execution exist on http, code and frontend_rpc and on nothing else, because those three are the only kinds that produce a result. There is nothing for the other eight to hide, to publish out of, or to stop waiting for. then and else belong to an if in exactly the same way. silent: true still runs the operation and still publishes its fields; it only keeps the response away from the model. Use it for a noisy intermediate lookup — a lookup whose raw JSON the agent has no business narrating.

on_error

abort (the default) stops the tool. Nothing after the failing operation runs, nothing further is published, and the model is told the tool could not be completed — the agent then apologises to the caller in its own words. continue logs the failure and moves to the next operation in the same chain. Two consequences worth knowing:
  • A failed operation publishes nothing. Anything a later template expected from it resolves to empty instead. Validation cannot see this, because it checks what an operation declares, not whether it ran.
  • On an if, there is no next operation — an if is terminal. on_error: "continue" on one means neither branch runs and the tree ends there. That is a real outcome to reach for, and it is the only thing continue can mean on an operation nothing follows.

The branching rule

if, handoff, end_call and transfer are terminal in their chain, and a chain is the top-level list or any then / else list. Nothing may follow one there — a tree that puts something after one is refused at save, naming the operations that came after it. Branches do not rejoin. There is no operation that both paths flow back into. So:
  • If work must happen on both paths, write it into both branches.
  • If work must happen after the decision, move the branch later instead.
  • For an early-exit guard, put the rest of the flow inside the guard’s else.
Anything the tool has to do before hanging up — logging the outcome, posting a lead, saving what the caller said — goes before the end_call, not after it. By the time an end_call returns, the session is already closing.

Derived silence

If no operation in the tree can return a response, the tool is silent whatever silent says — including operations inside if branches. A tool whose whole tree is one say "Your appointment is confirmed" says that once, rather than saying it and then having the model improvise a second sentence on top. You cannot turn derived silence off, and there is nothing to untick. If the agent should add a closing line, add a generate_reply operation that states what to add: it is visible in the tree, it says what it will say, and it costs the same turn. When a tool is not silent, the model is handed the ordered outputs as JSON — {"responses": [{"operation": "http", "response": { … }}, … ]} — or the literal string "Done." when a tool that could have returned something happened not to. A run that reaches end_call tells the model nothing either way; there is nobody left to narrate to.

Timeouts are in seconds

Every timeout in a tree is in seconds, never milliseconds. A caller is on the line while an operation runs, so a long timeout is not a patient tool, it is a hung call. Each one is capped: If an endpoint is genuinely slow, the answer is the tool-level long_running_task flag — the agent keeps talking while the work runs — not a 120-second timeout. See overview.

A three-branch tree

Look the account up, then take one of three paths. Notice that the second if is the only operation in the first one’s else: that is how you get a third branch, since nothing may follow an if in the same chain.
Three details in that tree:
  • The http publishes tier into userdata, so the rest of the conversation — and the next tool — can read it. balance_due goes to tooldata, because nothing outside this run needs it.
  • The http is silent, and it is the only operation that could return anything. So the whole tool is silent: each branch speaks for itself and the agent adds nothing after.
  • The enterprise branch runs two operations. A branch is an ordinary chain — it can be as long as you like, and the same terminal rule applies inside it.

Next

Templating and data

The six template roots, the two stores, and publish_fields.

Conditionals

Every comparison operator, and why time comparisons fail.

Testing and publishing

Run the draft tree and see what each operation did.

Patterns

Lookup-then-answer, guard-and-continue, escalation.