Agents
Create, configure, and run agents bound to a provider and a set of tools.
Overview
An agent is created from a provider plus its runtime config — model, instructions, sampling and step limits — and is a thin surface onto a runtime agent. Everything an agent is lives on the runtime (the source of truth) and is read from there when shaping responses; naturali stores nothing of its own about it, timestamps included.
Every Channels binding and customer app consumes agents from this registry. Runs happen through a Session; each model loop inside a session is a Generation.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Agents.
Data Model
Agent
| Field | Type | Description |
|---|---|---|
id | string | Public agent ID (agent_ prefix) — the runtime agent id. |
project_id | string | The owning project (proj_ prefix). |
provider_id | string | The provider this agent is bound to (aip_ prefix). |
name | string | Human-readable label. |
model | string, nullable | Model the agent generates with; null falls back to the provider default. |
instructions | string, nullable | System instructions guiding the agent's behavior. |
temperature | number, nullable | Sampling temperature (0–2). |
max_steps | integer, nullable | Maximum agent loop steps before stopping. |
max_context_messages | integer, nullable | Maximum recent messages included in the context window. |
tool_bindings | object[] | The tools bound to this agent — each { "tool_id": "tool_…" }. |
tool_choice | string | object, nullable | Tool choice strategy — "auto" / "required" / "none", or { type: "tool", name }. |
step_rules | array, nullable | Per-step tool_choice overrides — see Per-step tool choice. |
output_schema | object, nullable | JSON Schema the model's output is constrained to — see Structured output. null leaves it unconstrained. |
trace_content_mode | string, nullable | Agent-scope zero-retention setting: none never records this agent's content, full records it, null (the default) inherits the project — see Zero-retention agents. |
created_at | string (date-time) | |
updated_at | string (date-time) |
Key Concepts
Attaching tools
Attach tools to an agent with the agent's tool_bindings (and optional
tool_choice) on create or update. A binding names one tool by reference and
carries nothing else:
{ "tool_bindings": [{ "tool_id": "tool_V1StGXR8Z5jdHi6B" }] }
Each tool_id must be a tool registered in the same project; the
set is replaced wholesale on every update — [] detaches all tools. Any other
field inside a binding is a 400 naming it, so a misspelling cannot be stored
as a binding that resolves to nothing.
Per-step tool choice
tool_choice governs every step of a generation's tool loop, with no
relaxation once a step has called a tool — "required" forces a tool call on
every step, including once the model already has everything it needs, so the
run continues all the way to max_steps before it can stop. step_rules lets
one step override tool_choice on its own: setting [{ step: 1, tool_choice: "required" }] forces a tool call on the first step only, leaving the agent's
own tool_choice (e.g. "auto") in effect from the second step on — a model
that skips the tool it needs on turn one no longer gets the chance to, without
forcing every later turn into a tool call it doesn't need.
Both fields have exactly one spelling, and it is checked when you write them. A
rule carries step and tool_choice and nothing else; the object form of
tool_choice is { "type": "tool", "name": "…" } and nothing else. Anything
outside that — a camelCase toolChoice or toolName, a strategy string other
than auto, required or none — is a 400 naming the offending field, not
a value that is stored and then ignored. This matters more here than elsewhere:
a rule the runtime does not recognize is dropped rather than corrected, so a
misspelled one would leave the agent running normally while quietly never
forcing the tool call it was configured to force.
Structured output
Set output_schema to a JSON Schema and the agent stops returning prose: a
generation is constrained to that schema and the parsed
value comes back as object on the result.
- CLI
- SDK
- curl
naturali create-agent \
--project-id proj_V1StGXR8Z5jdHi6B \
--provider-id aip_V1StGXR8Z5jdHi6B \
--name triage-classifier \
--output-schema '{"type":"object","properties":{"approved":{"type":"boolean"}},"required":["approved"]}'
const { data: agent } = await naturali.agents.createAgent({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
provider_id: 'aip_V1StGXR8Z5jdHi6B',
name: 'triage-classifier',
output_schema: {
type: 'object',
properties: { approved: { type: 'boolean' } },
required: ['approved'],
},
},
});
curl -X POST \
https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/agents \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider_id": "aip_V1StGXR8Z5jdHi6B",
"name": "triage-classifier",
"output_schema": {
"type": "object",
"properties": { "approved": { "type": "boolean" } },
"required": ["approved"]
}
}'
A generation from that agent carries the parsed value:
{ "status": "completed", "text": "{\"approved\":true}", "object": { "approved": true } }
Three things are worth knowing before you bind a schema:
- It is agent configuration, not a per-call argument.
POST /v1/projects/{project_id}/agents/{agent_id}/generationsdoes not accept anoutput_schema; an agent either has one or it does not. Two shapes means two agents. Sendoutput_schema: nullon an update to clear it. - Only non-streaming generations get
object. Withstream: truethe reply streams as text chunks and the schema is not applied — the two features are mutually exclusive at the platform level. - A session turn is constrained too, but has nowhere to put
the object.
POST /v1/projects/{project_id}/agents/{agent_id}/sessions/{session_id}/generatereturnsmessage.contentand noobjectfield, so the JSON arrives as text in that string. That is fine for a program that parses it, and wrong for a human: a schema-bearing agent bound to a channel will post raw JSON into the conversation. Keep schemas on agents you call throughPOST /v1/projects/{project_id}/agents/{agent_id}/generations(or a board column), and leave channel-facing agents unconstrained.
The schema itself is validated by the model provider at generation time, not on
write — the subset of JSON Schema accepted there is narrower than the standard,
so a schema this API accepts can still be rejected when the agent runs. Only the
JSON type is checked up front: an object or null, else 400.
Zero-retention agents
trace_content_mode controls whether this agent's trace and
generation content — prompts, tool arguments, tool
results, error payloads — is recorded at all.
| Value | Meaning |
|---|---|
null (default) | Inherit the project's mode |
none | Zero-retention — this agent's content is never written |
full | Record content, even when other agents in the project do not |
Set it on
POST /v1/projects/{project_id}/agents or
PATCH /v1/projects/{project_id}/agents/{agent_id}.
This is the per-agent knob for the common case where one agent in a project
handles sensitive material and the rest do not — you get zero-retention where
you need it without giving up observability everywhere else.
Setting full on an agent whose project is none is refused with
400 invalid_trace_content_mode. The project mode is a floor: if it were
loosenable, a project-wide zero-retention mandate could be escaped simply by
creating a new agent.
Sending null restores inheritance — which is why null and omitting the
field mean different things on PATCH: omitting it leaves the current setting
alone. Tightening to none stops future writes but does not erase content
already recorded; purge that explicitly with
DELETE /v1/projects/{project_id}/traces/{trace_id}/content.
Deletion and dependent resources
Deleting an agent that still has dependent generations or traces returns
409 conflict. Pass force=true to delete those along with the agent — this
is destructive and irreversible.
Runtime defaults
Only provider_id is required on create; model, temperature,
max_steps, and max_context_messages all fall back to the provider or
platform default when omitted, so an agent can be created with a single
field and tuned later.
Examples
Create an agent
- CLI
- SDK
- curl
naturali create-agent \
--project-id proj_V1StGXR8Z5jdHi6B \
--provider-id aip_V1StGXR8Z5jdHi6B \
--name support-triage \
--tool-bindings '[{"tool_id":"tool_V1StGXR8Z5jdHi6B"}]'
const { data: agent } = await naturali.agents.createAgent({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
provider_id: 'aip_V1StGXR8Z5jdHi6B',
name: 'support-triage',
tool_bindings: [{ tool_id: 'tool_V1StGXR8Z5jdHi6B' }],
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/agents \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider_id": "aip_V1StGXR8Z5jdHi6B",
"name": "support-triage",
"tool_bindings": [{ "tool_id": "tool_V1StGXR8Z5jdHi6B" }]
}'
Get an agent
- CLI
- SDK
- curl
naturali get-agent \
--project-id proj_V1StGXR8Z5jdHi6B \
--agent-id agent_V1StGXR8Z5jdHi6B
const { data: agent } = await naturali.agents.getAgent({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
agent_id: 'agent_V1StGXR8Z5jdHi6B',
},
});
curl https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/agents/agent_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_API_KEY"
Make an agent zero-retention
- CLI
- SDK
- curl
naturali update-agent \
--project-id proj_V1StGXR8Z5jdHi6B \
--agent-id agent_V1StGXR8Z5jdHi6B \
--trace-content-mode none
const { data: agent } = await naturali.agents.updateAgent({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
agent_id: 'agent_V1StGXR8Z5jdHi6B',
},
body: { trace_content_mode: 'none' },
});
curl -X PATCH https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/agents/agent_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "trace_content_mode": "none" }'
Send trace_content_mode: null to go back to inheriting the project's setting.