Run a zero-retention agent
By the end of this tutorial you will have an agent whose prompts and replies are never written — proven by a run that kept its tokens, cost and timing but no message, read back beside a run of an ordinary agent that kept everything.
Four steps:
- Create a zero-retention agent.
- Run a generation — the caller still gets the reply.
- Read the transcript back — a skeleton, by design.
- Read the generation record — tokens and cost survive.
Then compare it with an ordinary agent.
Prerequisites
- A credential. A
nat_sk_…API key exported asNATURALI_TOKEN, and a client set up as in Create a provider → Prerequisites. - A project and a provider, exported as
PROJECTandPROVIDER— from Enable naturali models or Create a provider. - A generation of an ordinary agent, exported as
GENERATION— the one Your first agent generation ends with. It is the baseline in the last step.
export NATURALI_TOKEN=nat_sk_...
export PROJECT=proj_V1StGXR8Z5jdHi6B
export PROVIDER=aip_V1StGXR8Z5jdHi6B
export GENERATION=gen_1GPr5M1dYHdSjIJI
The setting lives on the agent, so the project and its other agents are left as they are.
1. Create a zero-retention agent
trace_content_mode: "none" means this agent's content — the messages it was
asked, every step, tool arguments and results — is never written. Leaving it
null inherits the project; full
stores even when the project does not. An agent can tighten a storing project to
none, never loosen a none project back to full.
- CLI
- SDK
- curl
naturali create-agent \
--project-id "$PROJECT" \
--ai-provider-id "$PROVIDER" \
--name intake-zero-retention \
--instructions 'Answer in one sentence. Never repeat personal data back.' \
--trace-content-mode none
const { data: agent } = await naturali.agents.createAgent({
path: { project_id: process.env.PROJECT! },
body: {
ai_provider_id: process.env.PROVIDER!,
name: 'intake-zero-retention',
instructions: 'Answer in one sentence. Never repeat personal data back.',
trace_content_mode: 'none',
},
});
curl -X POST "https://api.naturali.ai/v1/projects/$PROJECT/agents" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"ai_provider_id\": \"$PROVIDER\",
\"name\": \"intake-zero-retention\",
\"instructions\": \"Answer in one sentence. Never repeat personal data back.\",
\"trace_content_mode\": \"none\"
}"
{
"id": "agent_KX68YShE9FjK4yOv",
"project_id": "proj_7PftlMHzZV2K2yKA",
"ai_provider_id": "aip_wI7x72eOzIisxDPr",
"name": "intake-zero-retention",
"trace_content_mode": "none",
"version": 1
}
export AGENT=agent_KX68YShE9FjK4yOv
2. Run a generation
Send it something a compliance review would care about. ?wait=true holds the
request until the run finishes, so the reply comes back in this response.
- CLI
- SDK
- curl
naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--messages '[{"role":"user","content":"My card ends in 4242. Is my payment late?"}]'
const { data: run } = await naturali.agents.createAgentGeneration({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
query: { wait: true },
body: {
messages: [
{ role: 'user', content: 'My card ends in 4242. Is my payment late?' },
],
},
});
curl -X POST \
"https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT/generate?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messages": [{ "role": "user", "content": "My card ends in 4242. Is my payment late?" }] }'
{
"id": "gen_bFjYYBhB6XnnbqR5",
"trace_id": "trace_pqHrO7u38jyPYJJz",
"status": "completed",
"output": {
"model": "glm-4.7-flash",
"content": "No, I do not have access to your payment information or the status of your account.",
"finish_reason": "stop"
}
}
Zero-retention changes what is kept, not what the caller receives: the reply is in this response and nowhere else.
export ZR_GENERATION=gen_bFjYYBhB6XnnbqR5
3. Read the transcript back
GET /v1/projects/{project_id}/generations/{generation_id}/transcript
is where a run's content is read.
For this agent there is none to read.
- CLI
- SDK
- curl
naturali get-generation-transcript \
--project-id "$PROJECT" \
--generation-id "$ZR_GENERATION"
const { data: transcript } =
await naturali.generations.getGenerationTranscript({
path: {
project_id: process.env.PROJECT!,
generation_id: process.env.ZR_GENERATION!,
},
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/generations/$ZR_GENERATION/transcript" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"generation_id": "gen_bFjYYBhB6XnnbqR5",
"agent_id": "agent_KX68YShE9FjK4yOv",
"status": "completed",
"stop_reason": "stop",
"started_at": "2026-10-03T09:33:23.251Z",
"completed_at": "2026-10-03T09:33:23.582Z",
"step_count": 1,
"input": null,
"steps": [],
"output": null,
"content_redacted_at": "2026-10-03T09:33:23.245Z",
"content_redacted_by_principal_type": "system",
"content_redacted_by_principal_id": "zero_retention"
}
A 200 with the skeleton, not an error: input, steps and output are empty,
while the status, timing and step_count remain. zero_retention says the
content was never stored — a
purge later would name whoever
erased it instead. content_redacted_at predates started_at: the marker is set when the
row is created.
4. Read the generation record
The record the meter and the bill read from is untouched.
- CLI
- SDK
- curl
naturali get-generation \
--project-id "$PROJECT" \
--generation-id "$ZR_GENERATION"
const { data: generation } = await naturali.generations.getGeneration({
path: {
project_id: process.env.PROJECT!,
generation_id: process.env.ZR_GENERATION!,
},
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/generations/$ZR_GENERATION" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"id": "gen_bFjYYBhB6XnnbqR5",
"agent_id": "agent_KX68YShE9FjK4yOv",
"status": "completed",
"stop_reason": "stop",
"agent_version": 1,
"started_by_principal_type": "api_key",
"metadata": null,
"content_redacted_by_principal_id": "zero_retention",
"usage": {
"cost_usd": 9.77e-6,
"input_tokens": 31,
"output_tokens": 19
}
}
Tokens, cost, who started it and
which agent version answered
are all there. cost_usd is filled on a naturali-managed provider; on your own
credential your vendor bills the tokens
and it can be null.
5. Validate it against an ordinary agent
Read the transcript of the baseline GENERATION the same way. Its agent leaves
trace_content_mode at the default, so its content was stored.
- CLI
- SDK
- curl
naturali get-generation-transcript \
--project-id "$PROJECT" \
--generation-id "$GENERATION"
const { data: baseline } =
await naturali.generations.getGenerationTranscript({
path: {
project_id: process.env.PROJECT!,
generation_id: process.env.GENERATION!,
},
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/generations/$GENERATION/transcript" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"generation_id": "gen_1GPr5M1dYHdSjIJI",
"status": "completed",
"step_count": 1,
"input": [{ "role": "user", "content": "What is our refund window?" }],
"steps": [
{
"index": 0,
"text": "I am unsure of our refund window; this information may vary depending on the specific policies of the company or service in question.",
"finish_reason": "stop",
"tool_calls": [],
"usage": { "input_tokens": 19, "output_tokens": 26 }
}
],
"output": {
"content": "I am unsure of our refund window; this information may vary depending on the specific policies of the company or service in question.",
"finish_reason": "stop"
},
"content_redacted_at": null,
"content_redacted_by_principal_id": null
}
Same project, same read: the ordinary agent's transcript holds what it was asked and what it answered, and the zero-retention one holds nothing — not even the card digits it was sent. That difference is the whole value — content that was never written cannot leak, be missed by a cleanup or sit in a backup.
What's next
- Projects → content retention —
set
nonefor the whole project, or keep content for a window that a daily sweep enforces. The window has a ceiling set by your plan. DELETE /v1/projects/{project_id}/generations/{generation_id}/content— erase what an ordinary agent already stored, on request; the skeleton stays, marked with who erased it.- Traces → Purging a run's content — the same skeleton rule applies to the trace a generation belongs to.