Generations
One model loop inside a session — the record of an execution, not its live state.
Overview
A generation is created either directly
(POST /v1/projects/{project_id}/agents/{agent_id}/generations)
or as the effect of
POST /v1/projects/{project_id}/agents/{agent_id}/sessions/{session_id}/generate
(see Sessions). It carries lifecycle status, timing, the
structured error when it fails, and links to the Trace that
records every tool call it made.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Generations.
Data Model
Generation
| Field | Type | Description |
|---|---|---|
id | string | Public generation ID. |
project_id | string | The owning project. |
agent_id | string | The agent that ran this generation. |
trace_id | string | The trace recording this generation's tool calls. |
initiator_generation_id | string, nullable | The generation that triggered this one, for sub-agent chains. |
status | string | Lifecycle status — see GenerationStatus. |
stop_reason | string, nullable | Why the model loop stopped. |
error | object, nullable | Structured error when the generation failed. |
action_id | string, nullable | The action_id label you sent when running the generation, echoed back for spend rollups. |
trigger_id | string, nullable | The trigger that started this generation, when applicable. |
extraction | object, nullable | Memory-extraction summary (candidates, created, updated, skipped), when the agent's knowledge config produced one for this turn. |
metadata | object, nullable | Caller-supplied opaque metadata — nothing else. Server-owned state has its own top-level fields (action_id, trigger_id, extraction) and is never merged in here. |
content_redacted_at | string (date-time), nullable | When this generation's content was purged — see Purging generation content. Null when nothing has been purged. |
started_at | string (date-time), nullable | |
completed_at | string (date-time), nullable | |
last_activity_at | string (date-time), nullable | |
created_at | string (date-time) | |
updated_at | string (date-time) |
Key Concepts
Run : Trace : Generation
A generation is the innermost of three layers: a Trace is what one agent did inside an execution — a tree whose children are the traces its sub-agent calls started — and a generation is one model loop inside a trace. For a plain chat turn all three collapse to 1:1:1.
Structured output
Running a generation returns a GenerationResult, whose object is populated
when — and only when — the agent has an output_schema. The
model's output is constrained to that schema and the parsed value is handed back
alongside the raw text:
{ "status": "completed", "text": "{\"approved\":true}", "object": { "approved": true } }
An agent with no schema always reports object: null. Streaming generations
(stream: true) are never constrained, so they have no object either — see
Agents → Structured output.
Background by default
A model loop can outlast the request that starts it, so
POST /v1/projects/{project_id}/agents/{agent_id}/generations
does not hold one open. It answers 202 straight away with a handle:
{
"status": "accepted",
"generation_id": "gen_V1StGXR8Z5jdHi6B",
"agent_id": "agent_V1StGXR8Z5jdHi6B",
"trace_id": "trace_V1StGXR8Z5jdHi6B"
}
status is always accepted — it reports that the turn was queued, not how it
went. Read the outcome from
GET /v1/projects/{project_id}/generations/{generation_id},
polling until status leaves in_progress.
Pass wait=true to block instead and get the turn itself — the text, the
object, or the pending tool_calls — in one call:
- CLI
- SDK
- curl
naturali create-generation \
--project-id proj_V1StGXR8Z5jdHi6B \
--agent-id agent_V1StGXR8Z5jdHi6B \
--wait \
--messages '[{"role":"user","content":"What is the capital of France?"}]'
const { data: result } = await naturali.generations.createGeneration({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
agent_id: 'agent_V1StGXR8Z5jdHi6B',
},
query: { wait: true },
body: {
messages: [{ role: 'user', content: 'What is the capital of France?' }],
},
});
curl -X POST 'https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/agents/agent_V1StGXR8Z5jdHi6B/generations?wait=true' \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"What is the capital of France?"}]}'
Waiting is the right call for a short turn a person is sitting in front of. Prefer the default for anything long, anything with tools in the loop, or anything started from a request you do not want to keep open.
Two things do not change with the mode. Every reason to reject the request —
authentication, ownership, a malformed body — is still answered before the
202, so a bad request is never something you discover by polling. And
stream: true always waits, since a stream holds the request open by
definition; combining it with wait=false is a 400 rather than a guess.
Cost is per generation, and per project
Two grains, deliberately:
GET /v1/projects/{project_id}/generations/{generation_id}/usageprices one generation — the billing-grade receipt, with a line item per metered event and thequantity × unit_pricearithmetic behind each charge. Use it to answer "what did this turn cost".GET /v1/projects/{project_id}/usagerolls the whole project up bymodel,agent,run,dayormeter_type. Use it to answer "where did the money go".
The project meter cannot substitute for the receipt. Its finest bucket is a run,
a run can hold more than one generation — and a plain generation belongs to no
orchestration run at all, so group_by=run buckets it under a null key.
In both places cost_usd: null means nothing was priced — never that the work
was free. Only naturali-managed providers are priced; a
BYOK generation runs on your own provider account, so its
tokens are reported without a cost. Labelling a run with action_id when you
start it is what lets spend roll up per operating action later.
Tutorials → Your first agent generation walks the whole path, from provider to priced generation.
Purging generation content
DELETE /v1/projects/{project_id}/generations/{generation_id}/content
clears one generation's content — metadata, error, extraction and the
internal recovery state of a paused run — and stamps content_redacted_at as
verifiable proof the content is gone.
What survives is the billing and audit skeleton: ids, timestamps, status, stop
reason and the attribution fields (action_id, trigger_id) the usage ledger
reads. A purged generation still reads back with
GET /v1/projects/{project_id}/generations/{generation_id};
a 404 there would be indistinguishable from an id that never existed, and would
prove nothing about what was erased. That is also why content_redacted_at
matters on its own: without it you could not tell a purged generation from one
that simply never carried content.
The call is idempotent — purging an already-purged generation succeeds and
leaves the original content_redacted_at in place.
This is the narrow one, scoped to a single model turn. It does not delete
the parent trace's step payload, which holds this generation's content
alongside its siblings'. To erase a whole execution's content, purge the trace
with
DELETE /v1/projects/{project_id}/traces/{trace_id}/content,
which cascades to every descendant trace and all of their generations — see
Purging trace content.
Reach for the generation purge when an erasure request names one turn; reach for the trace purge when it names a run.
Examples
- CLI
- SDK
- curl
naturali get-generation \
--project-id proj_V1StGXR8Z5jdHi6B \
--generation-id gen_V1StGXR8Z5jdHi6B
const { data: generation } = await naturali.generations.getGeneration({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
generation_id: 'gen_V1StGXR8Z5jdHi6B',
},
});
curl https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/generations/gen_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_API_KEY"
And what that generation cost:
- CLI
- SDK
- curl
naturali get-generation-usage \
--project-id proj_V1StGXR8Z5jdHi6B \
--generation-id gen_V1StGXR8Z5jdHi6B
const { data: usage } = await naturali.generations.getGenerationUsage({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
generation_id: 'gen_V1StGXR8Z5jdHi6B',
},
});
console.log(usage?.cost_usd, usage?.total_tokens);
curl https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/generations/gen_V1StGXR8Z5jdHi6B/usage \
-H "Authorization: Bearer $NATURALI_API_KEY"
And erasing its content, keeping the billing skeleton:
- CLI
- SDK
- curl
naturali purge-generation-content \
--project-id proj_V1StGXR8Z5jdHi6B \
--generation-id gen_V1StGXR8Z5jdHi6B
const { data: purged } = await naturali.generations.purgeGenerationContent({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
generation_id: 'gen_V1StGXR8Z5jdHi6B',
},
});
// Proof the content is gone, while the usage record still prices the turn.
console.log(purged?.content_redacted_at, purged?.action_id);
curl -X DELETE https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/generations/gen_V1StGXR8Z5jdHi6B/content \
-H "Authorization: Bearer $NATURALI_API_KEY"