Turning it on
Turn records are the space’sturns feature, off by default, turned on in the features document by an approved diff of the security role. With it off, POST /v1/turns and the replay routes answer 404, and the SDKs record nothing, even with capture turned on in the code. The recording document, of the integration role, sets the content mode of the space and of each source, the required pins, how long the kept tier keeps a turn (kept_days, 30 by default, from 7 to 90), how long a replay scenario keeps its turns (scenario_days, 180 by default), whether the personal data model passes over stored records (pii_model) and the share of conversations kept whole by sample (sample_rate, 5% by default). Changing the content mode to one that keeps more also needs the security role.
What a turn is
A turn runs from its input to the last thing it emitted to the person or to a document. The input may be a message (kind: message), an interface action such as a button or a “show more” (action), a system event (event) or a timer that fired (timer). An interface action is a turn of its own, even with no model call. A sub-agent, or an agent called as a tool, opens a sub-turn, with its own turn_id and the turn that opened it in agent.parent_turn_id.
Capturing
The SDK captures in the agent’s process, at the moment each thing happens, by copying: a tool’s arguments and result are copied as JSON when the call returns, which costs a 25 KB result 0.05 ms at the 95th percentile; the digest is computed later, on the sender. When a result exists in two forms, both are recorded: the one the model saw (result_model) and the one the interface got (result_ui). A generator is recorded once it is fully consumed. Capture never delays the answer: sending happens later and apart from the turn, and a failure in recording marks the record completeness: incomplete without touching the agent.
turn_id is minted when the turn starts and follows the turn across asynchronous tasks and child sessions, so every call made on its behalf lands in the same record; in Python, a framework that runs tools on a thread pool uses niadra.turns.bind(fn), and in TypeScript the turn in progress follows the AsyncLocalStorage. The framework adapters open and close the turn for you with turns=True (turns: true): Google ADK, OpenAI Agents, LangGraph and LangChain in Python; LangChain and LangGraph, Mastra, the Vercel AI SDK, OpenAI Agents JS, Google ADK and VoltAgent in TypeScript. A function wrapped with tool() inside a framework’s tool takes over the call the adapter already recorded, so each call is recorded once; in a replay, LangGraph, Google ADK and Mastra tools answer from the record, and OpenAI Agents and VoltAgent tools must be wrapped with tool(), because their hooks cannot stop a tool. A model call is recorded by the adapter that sees its tokens. provenance turns a tool’s result into the objects it showed, each with the reference, the fields and the provenance; without provenance, an observation is display only and never updates typed state.
The record also keeps what the turn read from the memory, by version (the context by its ETag, a block by its version), the coordination decisions it relied on and the effects with the state of each, what the person was shown or did (the interactions) and the claim contract verdicts. What the turn said is not repeated: output.event_keys points at the events track() already sent.
The build and its pins
build.pins holds what must be the same for a replay to reproduce the turn: prompts (each prompt’s name and version), corpus_digest (a digest of the files the agent consults, computed by you, never the files), model (the exact model), assembler (the version of your context assembler) and tool_schemas (the digest of each tool’s schema). The SDK fills Niadra’s own pins itself: the context compiler’s version and the hash of the pack the turn read. The recording document says which pins are required (prompts and model by default); a turn without one of them is kept, marked not replayable, and the SDK warns once.
Content modes
The mode is space configuration, per source, and the SDK follows it. A blob is a large value of the record: a tool’s arguments, a result, the text of a read, a document the turn wrote.
A source may always send a mode that keeps less, never more: a turn refused with
content_mode_refused goes again with digests only. The digest is sha256: and the SHA-256 of the value’s canonical JSON (RFC 8785), so the same value has the same digest in any producer, and a replay matches calls by the args_hash of the normalized arguments. A turn too large on its own is sent with its blobs reduced to hashes, so a 413 turn_too_large never loops. A pointer or hash_only record is taken even with Niadra’s storage unavailable, because the frame is all of it. See Metadata only.
Fidelity and completeness
completeness says how much of the turn is in the record: complete, partial (the SDK dropped blobs to protect its queue; the frame stays), incomplete (the recording failed during the turn) or unknown (a bronze record).
The queue and the sending
Turns have their own queue, apart from the event queue, bounded by bytes (64 MB by default) and by count (2,000 turns). When it fills up, the SDK drops the blobs of unflagged turns first, oldest first, marking those recordspartial, and only then the oldest whole frames; both are counted. A flagged turn keeps its blobs longest, because it is the one someone will replay. Sending goes in batches of up to 50 records and 4 MB compressed, through POST /v1/turns, with the track scope; the answer is 200 when every record was taken, 207 with one error per rejected record. A turn Niadra already holds, by turn_id, is a duplicate: the same turn sent twice is one turn. A write process holds few large bodies (over 1 MB, as sent or decoded) at once; past that limit, the batch comes back with 429 and Retry-After, and the SDK sends it again.
The same code is in examples/turn_records.py and, in TypeScript, in examples/claim-guard.ts, which opens the turn and passes the answer through the claim contract.
Tiers and promotion
Every turn stays in a short tier, for 7 days. A turn moves to the kept tier, forkept_days, for one of three reasons: a flag set at capture (error, guard_acted, handoff, assertion_failed, synthetic, incomplete or negative_feedback), a later request, through POST /v1/turns/promote, naming a conversation or turn ids with the reason (complaint, bug_report, review or other), because the complaint arrives days after the turn, or the deterministic sample of whole conversations. After the tier’s retention, nothing of a turn remains except the day’s totals, which name no one. A legal hold keeps a conversation’s turns out of purging until it is released.
The turn.flagged webhook goes out for every turn kept by a flag, to the endpoints that subscribe: ids and flags only, never what the turn said or read.
The viewer
In the Console, the agents’ turns screen lists the kept turns, newest first, and opens each one: what it read, called, claimed and said, with the flags, the pins andreplay_blockers, why the turn cannot be replayed when it cannot (content_mode hash_only, fidelity bronze or silver, completeness partial, incomplete or unknown, or a required pin that is missing). Through the API, GET /v1/turns/{turn_id} and POST /v1/turns/search, with the conversation id in the body, need a key with the replay scope or a person with the integration or security role.
Replay
A scenario keeps up to 50 turns with the assertions they must keep passing; your CI runs each turn N times with the pinned build, inside your company, and Niadra decides the statistical verdict. Tools answer from the record, values never leave, and the result is never sent as a turn. See Replay in your CI.Cost and budget
Each record carries the turn’s cost in US dollars and the tokens of each model call. Thebudget block of a context read (include: ["budget"]) shows what the pack costs in estimated tokens, per section, and what this agent’s recorded turns already added up to in the conversation or the case (turns, model and tool calls, input, cached and output tokens, cost); counted: false says the counters could not be read, and a missing number is never zero. The block is shown, never enforced. Context use brings in cost the calls, tokens and money per turn, per source and agent, against the cost with memory off your company measured and declared in the measurement document.
Privacy
- The record repeats no conversation text:
output.event_keyspoints at the events. - In
pointermode, no recorded value reaches Niadra; inhash_only, no value is recorded. - Niadra never puts a conversation id in a URL or in a storage key in clear: turns live under a keyed hash of the conversation, and erasing a person erases their turns by that prefix.
- Records serve the purpose
quality, with the tier’s retention. A legal hold keeps them; once the subject is erased,retainedin the receipt counts the turns a hold still keeps. - The kept tier’s index enters your audit chain: each kept row has an entry (id, source, agent, kind, time, mode, build hash) with a SHA-256 digest, the rows of one UTC day form a Merkle root, and the daily
audit.rootevent carries it asturns.root, next to the receipts’ root.GET /v1/turns/index/{day}lists the rows with their digests so your company recomputes the root; a row that expires or is erased later leaves the anchored root as it was.
Next steps
Replay in your CI
scenarios, assertions and the statistical verdict.
Metadata only
the
pointer mode: the values in your bucket.Claims
the verdicts each turn carries.
Record turns
the reference of
POST /v1/turns.
