Tool calling
Tools are how agents reach outside the prompt — to query the knowledge graph, hydrate entities, propose actions, or delegate work. This page covers the mechanics: what a tool definition looks like, how multi-round calling works, and how errors flow back to the agent.
Tool definition shape
Every tool is defined with a JSON Schema parameter spec and a typed return:
{
name: 'graph_semantic_search',
description: 'Find entities by semantic + fuzzy match. Returns ranked nodes.',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Natural-language query.' },
kinds: { type: 'array', items: { type: 'string' }, description: 'Filter by node kind.' },
limit: { type: 'integer', default: 10, maximum: 50 },
},
required: ['query'],
},
}
The description field is what the model reads to decide when to
use the tool. Write it like a help line: short, action-oriented,
explicit about what the tool returns.
Multi-round semantics
The orchestrator can call multiple tools in a single user turn, serialized as a tool-use loop:
user: "Show me my pipeline this week"
└─ assistant turn 1
└─ tool_use(graph_semantic_search, {query: "deals updated this week"})
← tool_result: [12 nodes]
└─ tool_use(dashboard_query, {kind: "pipeline_by_stage"})
← tool_result: {by_stage: {...}}
└─ tool_use(read_entity, {id: "deal_abc", kind: "deal"})
← tool_result: {...full deal payload}
└─ assistant turn 1 (continued, with all 3 results in context)
└─ final text + citations
Cap is 8 tool rounds per user turn by default. Hitting the cap forces a final response — the model can't infinitely chain.
Parallel calls
The model often emits multiple tool calls in one response block when they're independent. The platform runs them in parallel and returns one tool_result block containing all results, preserving order. From the agent's view this is "one turn" — important for keeping the context window small.
Example: three semantic searches across different node kinds run concurrently → one round-trip latency instead of three.
Available tools (core)
| Tool | Purpose | When the orchestrator picks it |
|---|---|---|
graph_semantic_search | Find entities by question | Almost every run — the discovery primitive |
read_entity | Full payload by id | After search narrows to specific entities to dig into |
dashboard_query | Typed metric query | "How many X" / "show me top Y" questions |
propose_action | Write a planned action to TASKS | Anything that mutates real state |
delegate | Spawn a subagent | Sub-problem with a named workflow |
Surface-specific tools (some examples):
| Tool | Surface | Purpose |
|---|---|---|
write_card_draft | Today | Emit a daily card payload |
execute_planned_action | Inbox | Run a confirmed action |
query_kpi | Analytics | Run a registered KPI query |
compose_post | Content | Draft a social post |
Error handling
Tool failures return as tool_result blocks with an error field
and (when possible) a remediation hint:
{
"type": "tool_result",
"tool_use_id": "toolu_01...",
"is_error": true,
"content": "graph_semantic_search returned no matches. Try broader kinds: or a more general query."
}
The orchestrator can:
- Adjust parameters and retry (most common).
- Substitute a different tool.
- Answer with partial data, flagging the gap.
- Bail out to the user with "I tried X, here's what failed."
Transient errors (rate limits, 5xx) are handled by the runtime, not the model — the orchestrator sees a clean response or a final exhausted-retries error.
Idempotency
Tools that mutate state (propose_action, execute_planned_action,
write tools added by integrations) are idempotent via a
request_id that the platform generates per call. Retries with the
same request_id are deduplicated server-side, so a flaky network or
a model that re-emits the same tool call doesn't double-fire.
For external integration writes (sending an email, posting to a social network), idempotency is enforced via a per-tenant outbox table — the worker checks the outbox before executing, so even a worker restart mid-flight can't double-send.
Confirmation gate
propose_action is the platform's confirmation-gate hook. It NEVER
executes the action; it only writes the planned action to TASKS.
The actual execution path runs from the Inbox after user confirm.
See Tasks.
This is intentional — there's no "auto-send" tool. If a tool exists that mutates external state, it's marked as such in the registry and can only be invoked from a confirmation-gated execution context.
MCP tools
MCP servers extend the available tool set per-tenant. When a tenant connects an MCP server, its tools become visible to the orchestrator under a namespaced prefix:
mcp_<server>__<tool>
The orchestrator treats them like any other tool — they show up in
tool_use blocks and contribute to the same multi-round loop. The
8-round cap and idempotency contract apply equally.
Tool budget
The platform tracks tool call cost (LLM tokens spent reasoning
about each result + the result size in the context). When the
running budget approaches the per-turn cap, the runtime emits a
synthetic tool_result to the model saying "budget low, finish
with what you have" — the model wraps up rather than chaining
further calls. Hard cuts are rare; this is the soft brake.
Telemetry
Every tool call is logged in the TurnRecorder trace with:
- Tool name + input
- Latency
- Result size (bytes + truncated/full)
- Success / error / retry count
Visible in Settings → Agent runs → <run> → Tool calls. Useful when an agent run "feels slow" — usually one tool call dominates and surfaces immediately.
Read next
- MCP integration — how to plug in external tools.
- Today surface — the most tool-heavy surface, good to see the pattern end-to-end.