Skip to main content

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:

  1. Create a zero-retention agent.
  2. Run a generation — the caller still gets the reply.
  3. Read the transcript back — a skeleton, by design.
  4. Read the generation record — tokens and cost survive.

Then compare it with an ordinary agent.

Prerequisites​

  1. A credential. A nat_sk_… API key exported as NATURALI_TOKEN, and a client set up as in Create a provider → Prerequisites.
  2. A project and a provider, exported as PROJECT and PROVIDER — from Enable naturali models or Create a provider.
  3. 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.

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
{
"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.

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

naturali get-generation-transcript \
--project-id "$PROJECT" \
--generation-id "$ZR_GENERATION"
{
"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.

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

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