Chains
How far a continuation has run, and whether it is still spending.
Overview
A continuation chain is the population of generations that descend from one root because each continued the one before it. A chain opens the first time a generation continues another — an approved or expired tool call, a resumed session — so an ordinary one-shot generation is not a chain and gets no record. Gate a tool with guardrails approves a held call and reads the continuation it starts.
The module is read-only. There is nothing for a caller to create, and the size a chain may reach is set on the agent, not here.
This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Chains.
Data Model
Chain
| Field | Type | Description |
|---|---|---|
id | string | Public chain ID (chain_ prefix). |
project_id | string | The owning project. |
agent_id | string, nullable | The agent whose continuation opened the chain. Its origin, not an owner. |
status | string | active, concluded, expired or budget_exhausted. |
generation_count | integer | Generations in the chain, the root included. |
last_generation_at | string (date-time), nullable | When the chain last gained a generation. |
created_at | string (date-time) | |
updated_at | string (date-time) |
Key Concepts
The record describes the chain; it does not bound it
generation_count and status are observability. The ceiling is enforced by
counting the chain's member generations directly at the moment a hop is
attempted, so a stale or missing chain record can never let a runaway through.
The ceiling itself is an agent
stop condition —
{"type": "max_chain_generations", "max_generations": <n>} — and the deployment
carries its own limit. The effective ceiling is the smaller of the two, so an
agent can be stricter than the platform but never looser. A hop refused by it
stops with chain_limit, and the chain reads budget_exhausted afterwards.
concluded is not terminal
A chain is concluded when a member finished with nothing left pending. That is
a description of right now, not an ending: an approval decided months later can
spawn another hop and put the same chain back to active. Only
budget_exhausted and expired describe a chain that stopped for a reason.
expired means a held approval lapsed and the agent does not react to expiry,
so nothing resumed the chain.
A chain can span agents
agent_id names the agent whose continuation opened the chain, and later hops
may run on others. It is held as a plain id rather than a reference the platform
maintains, so deleting the agent leaves the chain record intact.
Expanding a chain into its members
The chain record carries the count, never the generations. To read them, filter
GET /v1/projects/{project_id}/generations
by chain_id. Every member carries the same chain_id — the continuations and
the root they descend from — and it is null on a generation that is not part of
a chain. Traces carry it too, so one filter follows a chain
through either surface.
Examples
Find the chains that may still be spending
- CLI
- SDK
- curl
naturali list-chains \
--project-id proj_V1StGXR8Z5jdHi6B \
--status active
const { data: chains } = await naturali.chains.listChains({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { status: 'active' },
});
for (const chain of chains?.data ?? []) {
console.log(chain.id, chain.generation_count, chain.last_generation_at);
}
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/chains?status=active" \
-H "Authorization: Bearer $NATURALI_TOKEN"
Read one chain, then its generations
- CLI
- SDK
- curl
naturali get-chain \
--project-id proj_V1StGXR8Z5jdHi6B \
--chain-id chain_V1StGXR8Z5jdHi6B
naturali list-generations \
--project-id proj_V1StGXR8Z5jdHi6B \
--chain-id chain_V1StGXR8Z5jdHi6B
const { data: chain } = await naturali.chains.getChain({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
chain_id: 'chain_V1StGXR8Z5jdHi6B',
},
});
const { data: members } = await naturali.generations.listGenerations({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { chain_id: chain?.id },
});
console.log(chain?.status, members?.data?.length, chain?.generation_count);
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/chains/chain_V1StGXR8Z5jdHi6B" \
-H "Authorization: Bearer $NATURALI_TOKEN"
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/generations?chain_id=chain_V1StGXR8Z5jdHi6B" \
-H "Authorization: Bearer $NATURALI_TOKEN"
Spot a chain a budget stopped
- CLI
- SDK
- curl
naturali list-chains \
--project-id proj_V1StGXR8Z5jdHi6B \
--status budget_exhausted
const { data: stopped } = await naturali.chains.listChains({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { status: 'budget_exhausted' },
});
for (const chain of stopped?.data ?? []) {
console.log(`${chain.agent_id} hit its chain ceiling at ${chain.generation_count}`);
}
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/chains?status=budget_exhausted" \
-H "Authorization: Bearer $NATURALI_TOKEN"