Skip to main content

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:

  1. Declare the function.
  2. Give it to the agent.
  3. Ask a question: the run pauses.
  4. 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​

  1. A credential. A nat_sk_… API key (or a session JWT from Auth) exported as NATURALI_TOKEN, and your client set up — the CLI, the SDK or plain curl against https://api.naturali.ai/v1:

    export NATURALI_TOKEN=nat_sk_...
    export NATURALI_API=https://api.naturali.ai/v1 # curl examples only
  2. A working agent. That is what Your first agent generation builds. Arrive with both ids exported:

    export PROJECT=proj_V1StGXR8Z5jdHi6B
    export 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.

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"]}'
{
"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.

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\"}]"
{
"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.

naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--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.

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\"}}]"
{
"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.

Submit once per pause

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.

naturali get-generation \
--project-id "$PROJECT" \
--generation-id "$GENERATION"
{
"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​