Run tools in your own code
By the end of this tutorial you will have an agent that calls a function your
code runs: the generation pauses at requires_action with the call's
arguments, you execute the function wherever your data lives, submit the
result, and the agent answers from it.
That is a client tool.
Nothing runs it server-side, so it reaches what the platform cannot — your
database, a service behind your firewall, a person who has to look something
up.
Four steps:
- Declare the function.
- Give it to the agent.
- Ask a question: the run pauses.
- Run the function and submit the result.
Then validate it. Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
-
A credential. A
nat_sk_…API key (or a session JWT from Auth) exported asNATURALI_TOKEN, and your client set up — the CLI, the SDK or plaincurlagainsthttps://api.naturali.ai/v1:export NATURALI_TOKEN=nat_sk_...export NATURALI_API=https://api.naturali.ai/v1 # curl examples only -
A working agent. That is what Your first agent generation builds. Arrive with both ids exported:
export PROJECT=proj_V1StGXR8Z5jdHi6Bexport AGENT=agent_V1StGXR8Z5jdHi6B
1. Declare the function
A client tool is a contract and nothing else: a name, a description and a
JSON Schema in parameters, with no execute. The model sees exactly this, so
write it the way you want the model to call your function. The argument keys
come back to you as you wrote them.
- CLI
- SDK
- curl
naturali create-tool \
--project-id "$PROJECT" \
--name get-order-status \
--type client \
--description 'Looks up an order in the store database and returns its status.' \
--parameters '{"type":"object","properties":{"order_id":{"type":"string","description":"The order id, e.g. ord_1042"}},"required":["order_id"]}'
const { data: tool } = await naturali.tools.createTool({
path: { project_id: PROJECT },
body: {
name: 'get-order-status',
type: 'client',
description:
'Looks up an order in the store database and returns its status.',
parameters: {
type: 'object',
properties: {
order_id: {
type: 'string',
description: 'The order id, e.g. ord_1042',
},
},
required: ['order_id'],
},
},
});
curl -sS -X POST "$NATURALI_API/projects/$PROJECT/tools" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "get-order-status",
"type": "client",
"description": "Looks up an order in the store database and returns its status.",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string", "description": "The order id, e.g. ord_1042" }
},
"required": ["order_id"]
}
}'
{
"id": "tool_IpZ6WO3CzlhcVtLY",
"project_id": "proj_cT9LACJi0WypPf5U",
"type": "client",
"name": "get-order-status",
"description": "Looks up an order in the store database and returns its status.",
"parameters": {
"type": "object",
"required": ["order_id"],
"properties": {
"order_id": {
"type": "string",
"description": "The order id, e.g. ord_1042"
}
}
},
"execute": null,
"created_at": "2026-10-03T09:35:21.748Z"
}
export TOOL=tool_IpZ6WO3CzlhcVtLY
2. Give it to the agent
Bind the tool and tell the agent when to use it, with
PATCH /v1/projects/{project_id}/agents/{agent_id}.
instructions replaces the agent's current ones.
- CLI
- SDK
- curl
naturali patch-agent \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--instructions "You answer questions about store orders. To learn an order's status, call get-order-status with its order_id, then answer from the result." \
--tool-bindings "[{\"tool_id\":\"$TOOL\"}]"
await naturali.agents.patchAgent({
path: { project_id: PROJECT, agent_id: AGENT },
body: {
instructions:
"You answer questions about store orders. To learn an order's status, call get-order-status with its order_id, then answer from the result.",
tool_bindings: [{ tool_id: TOOL }],
},
});
curl -sS -X PATCH "$NATURALI_API/projects/$PROJECT/agents/$AGENT" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"instructions\": \"You answer questions about store orders. To learn an order's status, call get-order-status with its order_id, then answer from the result.\",
\"tool_bindings\": [{ \"tool_id\": \"$TOOL\" }]
}"
{
"id": "agent_iYkY1p2BROb7vQLT",
"version": 3,
"instructions": "You answer questions about store orders. To learn an order's status, call get-order-status with its order_id, then answer from the result.",
"tool_bindings": [{ "tool_id": "tool_IpZ6WO3CzlhcVtLY" }],
"tool_choice": null
}
3. Ask a question: the run pauses
Run a generation with ?wait=true
(POST …/agents/{agent_id}/generate).
When the model calls a client tool, the answer is status: "requires_action",
and required_action.tool_calls lists what your code must run — each call's
id, tool_name and the model's args.
- CLI
- SDK
- curl
naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--messages '[{"role":"user","content":"Where is my order ord_1042?"}]'
const { data: paused } = await naturali.agents.createAgentGeneration({
path: { project_id: PROJECT, agent_id: AGENT },
query: { wait: true },
body: {
messages: [{ role: 'user', content: 'Where is my order ord_1042?' }],
},
});
const call = paused!.required_action!.tool_calls[0];
// call.tool_name === 'get-order-status', call.args.order_id === 'ord_1042'
curl -sS -X POST "$NATURALI_API/projects/$PROJECT/agents/$AGENT/generate?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "messages": [{ "role": "user", "content": "Where is my order ord_1042?" }] }'
{
"id": "gen_ZjWJKarBKQSbUFxB",
"trace_id": "trace_Sm0VzE5mYDaDHhUA",
"status": "requires_action",
"ai_provider_id": "aip_CLcDNRe8GpP1pBNs",
"required_action": {
"type": "submit_tool_outputs",
"tool_calls": [
{
"id": "tooluse_rbagPDjZvUZUrzrOQlAYAE",
"tool_name": "get-order-status",
"args": { "order_id": "ord_1042" }
}
]
}
}
export GENERATION=gen_ZjWJKarBKQSbUFxB
export CALL=tooluse_rbagPDjZvUZUrzrOQlAYAE
Nothing is running now. The generation is parked, and reading it with
GET /v1/projects/{project_id}/generations/{generation_id}
shows status: "requires_action" until you answer. The model decided to call
the tool on its own; if it answered without calling it, ask again with an order
id in the question, or force the call
with tool_choice.
4. Run the function and submit the result
Your code does the real work — here, a lookup that returns the order — and
posts the result with the matching tool_call_id to
POST …/generate/{generation_id}/tool-outputs.
output can be any JSON value. The same generation resumes with the result in
context and answers.
- CLI
- SDK
- curl
naturali submit-agent-tool-outputs \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--generation-id "$GENERATION" \
--tool-outputs "[{\"tool_call_id\":\"$CALL\",\"output\":{\"order_id\":\"ord_1042\",\"status\":\"shipped\",\"carrier\":\"DHL\",\"eta\":\"2026-10-07\"}}]"
// Your function — a query against your own database, say.
const order = {
order_id: call.args.order_id,
status: 'shipped',
carrier: 'DHL',
eta: '2026-10-07',
};
const { data: done } = await naturali.agents.submitAgentToolOutputs({
path: {
project_id: PROJECT,
agent_id: AGENT,
generation_id: paused!.id,
},
body: { tool_outputs: [{ tool_call_id: call.id, output: order }] },
});
curl -sS -X POST "$NATURALI_API/projects/$PROJECT/agents/$AGENT/generate/$GENERATION/tool-outputs" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"tool_outputs\": [{
\"tool_call_id\": \"$CALL\",
\"output\": { \"order_id\": \"ord_1042\", \"status\": \"shipped\", \"carrier\": \"DHL\", \"eta\": \"2026-10-07\" }
}]
}"
{
"id": "gen_ZjWJKarBKQSbUFxB",
"trace_id": "trace_Sm0VzE5mYDaDHhUA",
"status": "completed",
"ai_provider_id": "aip_CLcDNRe8GpP1pBNs",
"output": {
"model": "glm-4.7-flash",
"content": "Your order ord_1042 has been shipped! Here are the details:\n\n- **Status:** Shipped\n- **Carrier:** DHL\n- **ETA:** October 7, 2026\n\nThe order is currently in transit via DHL, and you can expect delivery by October 7th.",
"finish_reason": "stop"
}
}
The carrier and the date came from your output — nothing in the question or
the instructions mentioned them.
Posting outputs to a generation that has already completed runs the model again
on them and answers 200 with a new reply. Treat the submit as the one hand-off
for each pause, and make your code retry only on a failed request.
5. Validate it
Read the generation back. It is the same id that paused, now completed — the
run resumed rather than starting over — with its usage covering both halves.
- CLI
- SDK
- curl
naturali get-generation \
--project-id "$PROJECT" \
--generation-id "$GENERATION"
const { data: record } = await naturali.generations.getGeneration({
path: { project_id: PROJECT, generation_id: GENERATION },
});
curl -sS "$NATURALI_API/projects/$PROJECT/generations/$GENERATION" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"id": "gen_ZjWJKarBKQSbUFxB",
"agent_id": "agent_iYkY1p2BROb7vQLT",
"agent_version": 3,
"status": "completed",
"stop_reason": "stop",
"error": null,
"usage": {
"cost_usd": 0.00007468,
"input_tokens": 524,
"output_tokens": 95
},
"started_at": "2026-10-03T09:35:23.485Z",
"completed_at": "2026-10-03T09:35:34.739Z"
}
Resuming generates again, so it counts as a run on your plan like any other generation.
What's next
- Inside a conversation that keeps history, the same loop runs on a
session:
POST …/sessions/{session_id}/tool-outputs. - A tool that runs on the platform instead — an HTTP call with its URL and
secrets configured once — is an
httptool; see Debug a failed run for one in action. - Before your code runs a risky call, a person can approve it: Gate a tool with guardrails.