operations of a real tool. Read
the “when” and the “what it costs” lines before you paste — the trees are cheap,
the design decisions are not.
1. Lookup then answer
When. The most common shape there is: the caller asks something you have to fetch, and the agent answers from what came back.say is a filler whose whole job is to cover the HTTP latency, so it has no
wait_for_playback: the tree moves on while it plays. The http is silent, so
the raw JSON never reaches the model — only the two values you picked out of it.
What it costs on a live call. The HTTP round trip, plus one extra model call
for the generate_reply. If the wording never changes, swap the
generate_reply for a say and you save that model call and its latency.
2. Book, then confirm
When. The tool does something irreversible and the caller has to be told whether it worked.say, not a
generate_reply, because a reference number is exactly the thing a model
paraphrases wrongly. And the reference goes into userdata, not tooldata,
so the rest of the call — the agent’s prompt, another tool, a later turn — still
knows it after this tool returns.
What it costs. One HTTP round trip on the happy path and no model call at
all: the say branch is the whole reply. The failure branch costs one extra
model call, which is the right place to spend it.
An
if is terminal in its chain, so anything both branches need is written into
both. There is no place after the if to put shared work.3. Escalation
When. The caller needs a person. See transfer for the whole story; this is the shape.description— this is the escalation policy, and the only thing deciding when a caller reaches a person. Not “Transfer to a human”, but: “Use only after you have tried to answer the question yourself and the caller has asked for a person, or is clearly distressed. Do not use it for order status, delivery dates or returns — you can answer those.”disable_interruptions: trueon the tool, so a caller saying “hello?” over the ringback cannot cost them the outcome.
transfer_to_billing, transfer_to_sales, transfer_to_support.
What it costs. The call is over for the agent the moment it connects, and
lands on the transferred close reason. mode: "warm" costs a second AI
conversation on your own provider keys, plus however long the briefing takes,
and it holds the caller for all of it.
4. Guard, then continue
When. There is a precondition, and the tool should bail out early rather than do half the work. Because anif is terminal, an early exit is written the other way round from
how you would write it in code: the guard’s failure goes in then, and the
entire rest of the flow goes in else.
then because the guard is written positively
(“account exists”). Write it whichever way reads better — what matters is that
the flow lives inside a branch, since nothing can follow the if.
exists is true when the value is not null, not an empty string, and not an
empty list or object.
What it costs. Nothing when the guard passes: the comparison is local. The
guard is also the cheapest place to stop — one wasted model call beats one
wasted booking.
5. Business hours in a code operation
When. Any time-of-day or day-of-week decision. Do not try to do this with
an if: gt and lt need two numbers, so comparing "7:26 PM" against
"09:00" fails the operation rather than quietly taking the else.
code operation has no template syntax —
write input.system_vars.now, never {{system_vars.now}}, because braces inside
the TypeScript are neither substituted nor flagged. And the agent must have a
timezone set, or the clock system variables do not resolve at all; publish
refuses a tree that reads one without it.
What it costs. A round trip to the sandbox, in the tens of milliseconds. It
is by far the cheapest way to make a time decision, and the only correct one.
6. Silent logging
When. The tool records something and the caller should never know it ran — a disposition, an analytics event, a CRM touch.background_execution detaches the request: the tree returns immediately and the
caller waits for nothing. Nothing is published — a detached operation has no
result to publish out of, and pairing background_execution with
publish_fields is refused at publish. A failure only reaches the logs.
Tick silent as well. Silence is derived from the tree, and an operation left at
the default silent: false keeps the tool out of the derived-silent case — the
model would then be handed a bare "Done." and would say something about it.
What it costs. Nothing measurable on the call. This is the one operation the
author cannot observe on a live call, which is what makes the
test run worth doing — a test runs a background
operation in the chain so you can see it work.
The three rules behind all of this
One meaningful capability per tool.book_appointment that checks
availability, books and confirms beats check_availability +
create_booking + send_confirmation called in sequence, because sequencing is
the part the model gets wrong. Chain the steps in the tree, where the order is
yours.
One tool per transfer destination. Never one tool that branches on a
department argument. The model picks between named tools far more reliably than
it fills in an enum, every destination gets its own description and therefore its
own escalation policy, and the tool list ends up reading like the org chart.
Routing the model should decide belongs on the agent, not in a tree. If the
caller can ask for a desk — “I want to talk about my bill” — put the destination
on the agent’s handoffs field: one line per edge, and the
model routes it. The handoff operation is for the
case where the tree decides, after a lookup or inside an if. Building a tool
whose only job is to let the model choose a destination is re-implementing
handoffs with more moving parts.
Next
Testing and publishing
Run any of these against your real endpoints before you publish.
The operation tree
The eleven kinds,
on_error, and why branches do not rejoin.Transfer
The escalation recipe in full.
Templating and data
tooldata vs userdata, and how publish_fields resolve.