stella dataset
Build a redacted, source-stamped training dataset from this workspace's own records — one JSONL record per accepted turn, plus a manifest stating the exact rule that selected them.
stella dataset turns the records a workspace has already collected into a training-ready set: one JSONL record per accepted turn, every string scrubbed by the secret redactor, and a manifest.json beside it that states the selection rule in full.
It works offline: it reads .stella/private/store.db only, needs no API key, and never writes to the store. The export stays on your machine.
A person must sign off before any dataset is used for training. The manifest exists so that review is possible. Read it before you send the JSONL file anywhere.
Synopsis
stella dataset export --output <dir> \
[--format jsonl] [--since <timestamp>] [--until <timestamp>] [--require-verdict] \
[--include-unverified-transcripts]What it does
stella dataset export --format jsonl --output ./trajectorieswrites two files into ./trajectories, both owner-only (0600) inside an owner-only directory (0700):
| File | What it is |
|---|---|
dataset.jsonl | One JSON record per accepted turn, in ascending order by execution id |
manifest.json | The selection rule, the redactor's fingerprint, and the store's latest timestamp |
stella dataset export
--output <dir>RequiredDirectory to write dataset.jsonl and manifest.json into. Created owner-only if missing.
--format <jsonl>Record encoding.
Default jsonl
--since <timestamp>Keep only turns started at or after this timestamp. Compared as plain text against
executions.started_at, which SQLite writes as UTC YYYY-MM-DD HH:MM:SS, so a bare date works.
--until <timestamp>Keep only turns started strictly before this timestamp. The window includes the start but not the end.
--require-verdictAlso require a verifier result of passed. Off by default, because a turn the deterministic
check already cleared never reaches a verifier at all.
Default off
--include-unverified-transcriptsAlso export otherwise-accepted turns whose transcripts can't be checked against their digest, with calls
left empty and transcript_verified set to false. Off by default, because an unchecked transcript should never reach
a training set without saying so.
Default off
What counts as an accepted turn
The store has no "accepted" column, so the rule is named in one place and written into the manifest word for word:
executions.outcomein goal_met and at least one mutatingfile_changeevent and at least one recorded model call, every one rebuilt and verified against its digest
All three parts matter. A turn that reported success but changed nothing is a real result, but a poor training example, since there's no change to learn from. Every other outcome the engine writes (goal_unmet, verification_failed, aborted, cancelled, failed, error, interrupted) is left out. And a turn that can't prove what its model calls actually saw can't contribute a training pair, so it's left out too and counted in the manifest as executions_transcripts_unverified.
--require-verdict adds a fourth condition. --include-unverified-transcripts loosens the third. Either flag rewrites the rule shown in the manifest to match. The manifest always states the rule that actually ran.
Turns that can't vouch for their transcripts
The transcript condition is what a store from before receipts existed can't satisfy. With no step_receipt rows there's no recorded model call at all, so the default filter exports zero records from it, even though the journal still holds every one of those turns' prompt, tool calls, and changes.
--include-unverified-transcripts exports them anyway, with the transcript left out rather than guessed at:
callsis[]. An unverified transcript is withheld entirely, never sent with a silent gap in it.transcript_verifiedisfalse, so the record states why.transcript_mismatch_severityreports what a digest mismatch here means:none,compaction(the journal predates recording its own rewrites, so the mismatched bytes are an expected leftover from before compaction), orintegrity(compaction is no longer a possible explanation).
The rule that decides what to include doesn't treat old and new journals differently: a harmless compaction record is left out by default exactly like an integrity one. The severity is reported so you can tell them apart, never used to include one. tool_calls, changes, and verdict come from the journal and are just as trustworthy on these records as on any other.
reward is the one field that isn't purely read from the journal, and on records from before receipts existed, it says so. Its outcome comes from the journal's verdict, but its scoring depends on the step count, which comes from the receipts. A record with no receipts hasn't recorded that the turn made zero calls; it has recorded nothing at all. Treating that as zero would make the step penalty disappear, handing these records a score higher than the same trajectory would earn once its calls are counted. So cost.steps is null and the label reports discard: "steps_unknown" instead, keeping its tier, outcome, and rule so the row can still be picked and re-scored later under a count you supply. A record whose receipts exist but fail to verify isn't affected this way: its calls were recorded, so its step count is real and its reward scores normally.
The record
Every record carries the same keys. A missing value is written as null, never left out, so you never have to guess whether something is missing or simply doesn't apply.
{
"schema": 3,
"execution_id": 412,
"session_id": "ses-9f2c",
"repo": "3a7e1c04b9d2f5e8",
"timestamp": "2026-08-02 14:30:00",
"turn_instance": 0,
"kind": "run",
"provider": "zai",
"model": "glm-5.2",
"outcome": "completed",
"cost_usd": 0.0143,
"prompt": "make the flaky retry test deterministic",
"calls": [
{
"turn_instance": 0,
"step": 0,
"call_seq": 0,
"role": "worker",
"provider": "zai",
"model": "glm-5.2",
"prompt_messages": [
{ "role": "system", "content": "…" },
{ "role": "user", "content": "make the flaky retry test deterministic" }
]
}
],
"transcript_verified": true,
"transcript_mismatch_severity": "none",
"tool_calls": [
{
"seq": 12,
"turn_instance": 0,
"name": "edit",
"call_id": "call_3",
"arguments": { "path": "src/retry.rs", "old_string": "…", "new_string": "…" },
"output": "edited src/retry.rs",
"is_error": false,
"duration_ms": 31
}
],
"changes": [
{
"path": "src/retry.rs",
"kind": "modified",
"added": 6,
"removed": 2,
"diff": "@@ …",
"diff_truncated": false
}
],
"verdict": { "passed": true, "deterministic": true, "summary": "cargo test flipped fail -> pass" },
"reward": {
"rung": "submit_fast",
"outcome": 1.0,
"reward": 0.93285,
"cost": { "steps": 3, "cost_usd": 0.0143, "revisions": 0 },
"policy": {
"outcome": { "deterministic": 1.0 },
"shaping": { "per_step": 0.02, "per_usd": 0.5, "per_revision": 0.1 }
}
},
"redacted": false
}The transcript
calls lists every model call in the order it happened, each with the exact messages it was sent. These are rebuilt from receipts by the same process stella inspect uses, and are only included once their digest checks out. That's what makes a training pair derivable from a record alone. calls is empty exactly when transcript_verified is false, which only happens with --include-unverified-transcripts.
reward is the training score the settled verdict earned, computed under the workspace's own reward rules. The rules travel alongside the score, so a score stays meaningful even outside the workspace that produced it. It's null when the journal holds no verdict at all: there's nothing to build a score from, and making one up under rules the run never saw would claim more than the store actually knows. The raw verdict is included alongside it so you can build your own score under different weights. A record can have a label but still carry no score: reward.reward is null next to a named reward.discard whenever the trajectory can't be priced, including the steps_unknown case described above.
Where a record comes from
session_id, repo, timestamp, and turn_instance trace every record back to the episode it came from, which is what makes a distilled model auditable.
repo is a hash of the workspace's normalized origin URL, the same identity used for telemetry, so the dataset can be tied to a repository without carrying the URL itself. A workspace with no remote falls back to a hash of its path. session_id is null for a one-shot stella run, which never stamps one. That's a fact about the turn, not missing data.
What changes can tell you
There's no full diff anywhere in the store. diff is the recorder's changed-region excerpt, capped at 200 lines per side. added and removed are the recorder's own counts and are the numbers to trust. Don't recompute them by counting +/- lines yourself. diff_truncated is true when the excerpt clearly shows fewer changed lines than were counted. false means "no sign of truncation," not "guaranteed complete." The exact bytes the model produced are still available in the edit and write tool arguments.
Redaction
Every string in every record, and in the manifest, passes through the same secret redactor the rest of the workspace uses, applied after the record is fully built rather than field by field, so a field added later can't slip past it. A planted test API key in a prompt or a tool argument won't reach the output.
redacted on each record reports whether anything was actually replaced, so "this was scrubbed" is a fact you can see rather than something you have to assume.
The manifest's redaction.behavior_digest fingerprints the redactor by its decisions on a fixed test set of text, so it changes whenever a threshold, a matcher, or a rule list changes. One limit: a rule added for a vendor that test set doesn't cover leaves the digest unchanged.
The manifest
{
"schema": 3,
"format": "jsonl",
"file": "dataset.jsonl",
"records": 37,
"repo": "3a7e1c04b9d2f5e8",
"filter": {
"acceptance_predicate": "executions.outcome in {completed, goal_met} AND at least one mutating file_change event AND at least one recorded model call, every one reconstructing digest-verified (Reconstruction::is_verified)",
"since": null,
"until": null,
"require_verdict": false,
"include_unverified_transcripts": false,
"executions_scanned": 214,
"executions_in_window": 214,
"executions_transcripts_unverified": 6,
"executions_accepted": 37
},
"redaction": {
"function": "stella_learn::redact::redact_secrets",
"placeholder": "[redacted]",
"behavior_digest": "9c1f0a4b7e2d6538"
},
"store_watermark": "2026-08-02 15:02:11",
"execution_id_range": [178, 412]
}executions_transcripts_unverified counts the same executions in both modes. Under --include-unverified-transcripts these are the records that carry transcript_verified: false rather than records left out entirely, so the two modes stay comparable.
schema goes up whenever the record shape changes in a way that breaks older readers, so a reader can refuse a dataset it doesn't understand instead of misreading it.
store_watermark is the store's own most recent timestamp, not the current time. The export never reads the clock, so running it again against the same store produces the exact same files.
Consistent output
The same store and the same filter always produce byte-for-byte identical output. Executions are ordered by id, tool calls and changes by journal order, and each record is written from its typed structure so keys always appear in the same order. Nothing in the export reads the clock or writes from an unordered collection.
See also
stella inspect— the exact context one past model call was sentstella tune— compare one setting across two result files and pick a winnerstella stats— what the recorded work cost