Run an agent that records nothing
By the end of this tutorial you will have an agent that answers normally and writes none of its content to disk — no prompts, no tool arguments, no results, no error payloads — while it still shows up in your audit trail and still prices on your bill.
This is a different claim from deleting. Erasing a person's data says "we deleted it", which is only as good as your ability to find every copy. Zero-retention says the content was never written, so there is nothing for a sweep to miss and nothing for a backup to keep. When content must not reach disk at all, that is the setting you want.
Three steps:
Each 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 — do it first if you haven't. Arrive here with both ids exported:
export PROJECT=proj_V1StGXR8Z5jdHi6Bexport AGENT=agent_V1StGXR8Z5jdHi6Bexport NATURALI_PROJECT=$PROJECT # lets the CLI omit --project-id
1. Put the agent into zero-retention
trace_content_mode is agent configuration, set once.
none means this agent's content is never written; null — the default —
inherits the project's setting.
- CLI
- SDK
- curl
naturali update-agent \
--agent-id agent_V1StGXR8Z5jdHi6B \
--trace-content-mode none
const { data: agent } = await naturali.agents.updateAgent({
path: { project_id: PROJECT, agent_id: AGENT },
body: { trace_content_mode: 'none' },
});
curl -X PATCH "$NATURALI_API/projects/$PROJECT/agents/$AGENT" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "trace_content_mode": "none" }'
{
"id": "agent_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "intake",
"max_steps": 20,
"trace_content_mode": "none",
"status": "active",
"updated_at": "2026-07-20T15:09:48.868Z"
}
That is the whole configuration. Everything else about the agent — its model, its tools, its instructions — is unchanged, and it will keep answering exactly as it did.
This is not free. With none set, traces stop carrying
the step payload, so when this agent does something wrong you cannot look at
what it did — no tool arguments, no results, no error text. Debugging becomes
guesswork.
Turn it on for the agent that handles material you must not store, not for the whole fleet by reflex. Per-agent is the grain for exactly this reason: the sensitive agent goes dark while the rest stay debuggable.
2. Run it on something sensitive
Run a generation the way you normally would. Nothing about the call changes — that is the point.
- CLI
- SDK
- curl
naturali create-generation \
--agent-id agent_V1StGXR8Z5jdHi6B \
--wait \
--messages '[{"role":"user","content":"Patient reports chest pain. Summarise in one line."}]' \
--action-id intake.triage \
--metadata '{"case_ref":"MRN-88421"}'
const { data: result } = await naturali.generations.createGeneration({
path: { project_id: PROJECT, agent_id: AGENT },
query: { wait: true },
body: {
messages: [
{
role: 'user',
content: 'Patient reports chest pain. Summarise in one line.',
},
],
action_id: 'intake.triage',
metadata: { case_ref: 'MRN-88421' },
},
});
curl -X POST "$NATURALI_API/projects/$PROJECT/agents/$AGENT/generations?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "role": "user", "content": "Patient reports chest pain. Summarise in one line." }
],
"action_id": "intake.triage",
"metadata": { "case_ref": "MRN-88421" }
}'
{
"id": "gen_V1StGXR8Z5jdHi6B",
"agent_id": "agent_V1StGXR8Z5jdHi6B",
"status": "completed",
"text": "The patient is experiencing chest pain.",
"object": null,
"tool_calls": null
}
The answer came back. Zero-retention governs what is written, not what is
returned — your caller gets the reply exactly as before. Note that the
prompt named a patient and the call attached case_ref: MRN-88421; the next
step is where you check what became of them.
export GENERATION=gen_V1StGXR8Z5jdHi6B
3. Confirm nothing was stored
Read the generation record back.
- CLI
- SDK
- curl
naturali get-generation \
--generation-id gen_V1StGXR8Z5jdHi6B
const { data: generation } = await naturali.generations.getGeneration({
path: { project_id: PROJECT, generation_id: GENERATION },
});
curl "$NATURALI_API/projects/$PROJECT/generations/$GENERATION" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"id": "gen_V1StGXR8Z5jdHi6B",
"trace_id": "trace_V1StGXR8Z5jdHi6B",
"status": "completed",
"stop_reason": "stop",
"error": null,
"action_id": "intake.triage",
"extraction": null,
"metadata": null,
"content_redacted_at": "2026-07-20T15:09:59.330Z",
"started_at": "2026-07-20T15:09:59.338Z",
"completed_at": "2026-07-20T15:09:59.783Z"
}
Three things to read here:
metadataisnull. You sent{ "case_ref": "MRN-88421" }and the record kept nothing.errorandextractionare empty for the same reason.content_redacted_atis stamped, and it is earlier thanstarted_at. That ordering is the proof of the stronger claim: the marker went on when the record was created, before the turn ran. Nothing was written and then cleared — there was never anything to clear.- The audit and billing skeleton survives.
status,stop_reason, the timestamps and theaction_idyou labelled the spend with are all intact, so the run is still attributable and still priced. Check it withGET /v1/projects/{project_id}/generations/{generation_id}/usage— see Your first agent generation.
The trace tells the same story: step_count still
counts the steps, has_steps is false, and content_redacted_at is stamped.
The record that work happened survives; what the work contained never existed.
GET /v1/projects/{project_id}/traces/{trace_id}/steps
answers 404 steps_redacted here, with content_redacted_at in details.
That is a different code from steps_not_available, which is the brief window
after a normal run where the payload really is still being written. Retrying
that one works; retrying this one never will. If you have a client that polls
for steps, branch on the code rather than waiting it out.
What's next
- Make it the rule, not the exception. Setting
trace_content_mode: noneon the project applies it to every agent in it, and becomes a floor: an agent may tighten tononebut can no longer loosen tofull, so the mandate cannot be escaped by creating a new agent. - Deal with what you already stored. Turning this on stops future writes and
changes nothing about the past. Erase the backlog on demand with
Erase a person's data, or give the project a
trace_content_retention_dayswindow and let the daily sweep do it. - Remember what this does not cover. These settings govern what this platform stores. The model still receives the prompt, and what the provider does with it is governed by your agreement with that provider — directly with them, for a BYOK provider.