CLI
The strata CLI does two jobs. It checks exported graph definitions, so you can fail a build on a broken graph. And it reads the threads and checkpoints in a store, so you can see where a thread stopped and what its state was. It’s one binary, and it works the same for C# and Rust applications, because both engines write the same checkpoint format. It’s read-only: it never changes a thread.
To install it, see Install the strata CLI.
Commands
Section titled “Commands”| Command | What it does |
|---|---|
strata graph view |
Draws a graph definition in the terminal. |
strata graph validate |
Checks one graph definition, or a folder of them. |
strata thread list |
Lists the threads in a store, with filters. |
strata thread inspect |
Shows one thread’s latest checkpoint in full. |
strata checkpoint log |
Lists every checkpoint of one thread. |
strata checkpoint diff |
Shows the state fields that changed between two checkpoints. |
strata checkpoint replay |
Shows the state and the next node as of one checkpoint. |
Common options
Section titled “Common options”| Option | Meaning |
|---|---|
-h, --help |
Prints help for the CLI, a command group, or a command: strata --help, strata thread --help, strata thread list --help. |
-V, --version |
Prints the version, for example strata 0.9.0-preview.1. |
--json |
Prints JSON instead of text. Every command takes it. The shape of each command’s JSON is in its section below. |
--storage <STORAGE> |
The store to read. Required by the thread and checkpoint commands. See Store paths and URLs. |
--blobs <DIR> |
The folder of a filesystem blob store. Taken by thread inspect and the checkpoint commands. See Offloaded values. |
Errors go to standard error and start with strata:. Results go to standard output, so you can pipe --json output to a tool such as jq.
Store paths and URLs
Section titled “Store paths and URLs”--storage takes a SQLite file path, or a Postgres or Redis URL:
| Value | Store |
|---|---|
checkpoints.sqlite |
A SQLite file. Any value that isn’t one of the URLs below is opened as a file path. |
postgres://user:password@db.internal:5432/strata |
Postgres. postgresql:// works too. |
redis://cache.internal:6379 |
Redis, with the default key prefix strata. |
redis://cache.internal:6379/0?prefix=acme |
Redis, with the key prefix acme. |
The CLI reads through the same Rust stores your application writes with, so it reads checkpoints from either engine.
A few things to know:
- A missing SQLite file is created. If you mistype the path,
thread listprintsNo threads found.instead of an error. Check the path when a store looks empty. - Opening a store sets it up, as it does in your application: for SQLite and Postgres, the CLI creates the tables if they’re missing, and upgrades them if they’re older than the CLI. It never writes a checkpoint.
- There is no TLS. The CLI can’t reach Postgres or Redis over TLS, and
rediss://URLs don’t work. See Known limitations. - The Strata Cloud store isn’t supported. Use a self-hosted store when you need the CLI.
Offloaded values
Section titled “Offloaded values”Your application can offload large state values out of the checkpoint row. By default, the CLI reads them from the store’s own database. If your application puts them in a filesystem blob store instead, give that folder with --blobs:
strata thread inspect transfer-acme --storage checkpoints.sqlite --blobs ./blobsthread list doesn’t take --blobs, because it doesn’t print state.
Exit codes
Section titled “Exit codes”| Code | graph validate |
graph view |
thread list |
thread inspect, checkpoint log |
checkpoint diff, checkpoint replay |
|---|---|---|---|---|---|
0 |
Every graph is valid. | The graph was drawn. | The query ran, even if it found no threads. | The thread was shown. | The checkpoints were shown. |
1 |
At least one graph has structural errors. | The thread has no checkpoints. | A checkpoint with that sequence doesn’t exist. | ||
2 |
A file can’t be checked, the path doesn’t exist, or a folder has no *.json files. |
The file can’t be read or isn’t a graph definition. | The store can’t be opened or queried, or --status isn’t a known state. |
The store can’t be opened or queried. | The store can’t be opened or queried. |
A mistyped command or a missing required option also exits with 2, after a usage message.
strata graph view
Section titled “strata graph view”strata graph view --path <PATH> [--json]| Option | Value | Default | Meaning |
|---|---|---|---|
--path |
A file | Required | The graph definition to draw. A folder isn’t accepted. |
--json |
Off | Prints the whole definition as JSON instead of a drawing. |
It draws the graph’s main path as boxes: it starts at the entry point and follows the first unguarded edge out of each node. Below the drawing, it lists:
- Additional edges: every edge that isn’t on the main path, with
(guarded: <condition>)for a guarded one. - Composite nodes: one line for each parallel group, subgraph, map, wait node and interrupt boundary. It names a group’s branches, a child graph’s entry point and size, and where a boundary’s reject and timeout routes go. It doesn’t draw child graphs.
--jsonprints them. - The graph’s ID and version on a
Graph:line at the top, when the definition has an identity.
strata graph view --path graphs/wire-transfer.json┌─────────────────────┐│ PrepareWireTransfer │└─────────────────────┘ │┌─────────────────────┐│ ValidateApproval │└─────────────────────┘ │┌─────────────────────┐│ ExecuteWireTransfer │└─────────────────────┘
Additional edges: ValidateApproval -> PrepareWireTransfer (guarded: retry_count < 3)A graph with an identity, interrupt boundaries and composite nodes prints lines like these below its boxes:
Additional edges: Approve -> Abort (guarded: on_reject) Approve -> Escalate (guarded: on_timeout)
Composite nodes: Approve: interrupt boundary, rejects to Abort, times out after 30m to Escalate Gather: parallel group [Logs, Metrics]; interrupt boundary Review: subgraph, entry Draft, 2 nodes; interrupt boundary, times out after 1h, then the thread endsstrata graph validate
Section titled “strata graph validate”strata graph validate --path <PATH> [--json]| Option | Value | Default | Meaning |
|---|---|---|---|
--path |
A file or a folder | Required | One graph definition, or a folder. For a folder, it checks every *.json file under it, in subfolders too. |
--json |
Off | Prints the result as JSON. |
It runs the same structural validator that compiling a graph runs, so it reports the same error codes. Compiling and validation lists them. It can’t see the checks compiling makes beyond those codes, such as a recovery policy on a node that isn’t fallible, because a definition doesn’t record them. It checks inside parallel groups, subgraphs and maps too, and names a node inside a child graph by its path, such as Remediate/Abort.
It refuses a file that isn’t a graph definition: one it can’t parse, one with a schema_version it doesn’t know, or one whose node fields don’t match the node’s kind. That exits with 2, not 1, because the file is broken, not the graph.
For one file, it prints OK or FAILED, and one line per error: [CODE] node -> related node: message.
strata graph validate --path graphs/billing.jsonFAILED: 'graphs/billing.json' has 1 structural error(s): [STRATA_ERR_DANGLING_EDGE] B -> Missing: Edge 'B' -> 'Missing' points at 'Missing', which is not a node in the graph.For a folder, it checks every file, sorted by path, so the report is the same on every machine. It reports every file, not only the first failure, and ends with a summary. It ignores files that don’t end in .json, and doesn’t follow symbolic links to folders. A folder with no *.json files exits with 2, so an empty export never passes.
strata graph validate --path graphsOK graphs/approval.jsonFAILED graphs/billing.json (1 structural error(s)) [STRATA_ERR_DANGLING_EDGE] B -> Missing: Edge 'B' -> 'Missing' points at 'Missing', which is not a node in the graph.OK graphs/incident.jsonERROR failed to parse 'graphs/notes.json' as a graph definition: missing field `schema_version` at line 1 column 74 graph file(s) checked: 2 valid, 1 invalid, 1 unreadable.This run exits with 2: an unreadable file outranks a graph with errors.
JSON output
Section titled “JSON output”For one file, --json prints valid, entry_point and errors. Each error has code, kind, node, related_node and message:
{ "entry_point": "A", "errors": [ { "code": "STRATA_ERR_UNGUARDED_CYCLE", "kind": "UnguardedCycle", "message": "Unguarded cycle detected between 'B' and 'A'. Add a guarded/conditional edge to protect this loop.", "node": "B", "related_node": "A" } ], "valid": false}For a folder, it prints valid, checked, valid_count, invalid_count, unreadable_count and graphs. Each entry in graphs has path and valid, then either entry_point and errors, or error for a file it couldn’t check.
Using it in CI
Section titled “Using it in CI”Your build exports each graph to a JSON file with ToDefinition() (C#) or to_definition() (Rust), without compiling it, so a broken graph is still written out. Graph definitions and CI explains the export. Then strata graph validate checks the folder, and a non-zero exit fails the job.
A GitHub Actions job on Linux, after your export step has written graphs/:
- name: Install the strata CLI run: | curl -fsSLO https://github.com/datalium/strata/releases/download/v0.9.0-preview.1/strata-linux-x64 curl -fsSLO https://github.com/datalium/strata/releases/download/v0.9.0-preview.1/SHA256SUMS.txt sha256sum --check --ignore-missing SHA256SUMS.txt install -m 755 strata-linux-x64 /usr/local/bin/strata
- name: Validate the graphs run: strata graph validate --path graphsThe 14-graph-ci-gate demo runs the whole gate, in C# and Rust.
strata thread list
Section titled “strata thread list”strata thread list --storage <STORAGE> [OPTIONS]| Option | Value | Default | Meaning |
|---|---|---|---|
--storage |
A path or URL | Required | The store to read. |
--status |
A state | All states | Only threads in this state. |
--due |
Off | Only waiting threads whose timer or timeout is due now. | |
--due-by |
An RFC 3339 time | Only waiting threads whose timer or timeout is due at this time, such as 2026-10-30T12:30:00Z. You can’t use it with --due. |
|
--meta |
KEY=VALUE |
Only threads whose latest checkpoint has this metadata pair. Repeat it to require several: a thread must have every pair. | |
--graph |
A graph ID | Only threads written by the graph with this ID. | |
--graph-version |
A version | Only threads written by this version of the --graph graph. It needs --graph. |
|
--json |
Off | Prints JSON. |
--status takes one of interrupted, failed, cancelled, running, completed, ended or resumed. These are thread states, not checkpoint statuses: a thread that was resumed from its latest checkpoint is resumed, not interrupted. A running thread is either still running, or its process died. Threads and checkpoints explains each state, and Durable execution explains recovering a dead running thread.
--due and --due-by match threads that are waiting (interrupted, not resumed) and whose wait is due by that time: a timer due to wake, or a person or an event whose timeout has passed.
--meta matches the engine’s own keys too, so --meta error_kind=transient finds threads that failed with a transient error. All the filters combine: a thread must match every one you give.
It prints one row per thread, most recently updated first. It reads every page of the store, so there is no limit to set.
strata thread list --storage checkpoints.sqlite+-----------------+--------------+-------------+-------------+------------------------------------------------------+----------------------+| Thread ID | Current Node | Sequence ID | State | Waiting For | Updated At |+-----------------+--------------+-------------+-------------+------------------------------------------------------+----------------------+| deploy-2.4.1 | Deploying | 0 | interrupted | event deployment.finished until 2026-10-30T12:00:00Z | 2026-10-30T11:00:00Z |+-----------------+--------------+-------------+-------------+------------------------------------------------------+----------------------+| transfer-globex | Prepare | 1 | failed | - | 2026-10-30T10:15:09Z |+-----------------+--------------+-------------+-------------+------------------------------------------------------+----------------------+| transfer-acme | Approve | 2 | interrupted | human until 2026-10-31T09:00:02Z | 2026-10-30T09:00:02Z |+-----------------+--------------+-------------+-------------+------------------------------------------------------+----------------------+| Column | What it shows |
|---|---|
| Thread ID | The thread. |
| Current Node | Where the thread stands: its suspended node, or its next node if it has none yet. For a thread stopped inside a subgraph, the path, such as Work/Approve. |
| Sequence ID | The sequence of its latest checkpoint. |
| State | Its thread state. |
| Waiting For | human, timer or event <type>, with until <time> when the wait has a deadline. - when the checkpoint records no wait, which includes an interrupt boundary without a timeout: such a thread shows interrupted in State and - here. |
| Updated At | When its latest checkpoint was written. |
When nothing matches, it prints No threads found. and exits with 0.
Some filtered examples:
# Failed threads of one tenantstrata thread list --storage checkpoints.sqlite --status failed --meta tenant=globex
# Threads of version 3 of the wire-transfer graphstrata thread list --storage checkpoints.sqlite --graph wire-transfer --graph-version 3
# Waiting threads whose timer or timeout is due by 12:30 UTCstrata thread list --storage checkpoints.sqlite --due-by 2026-10-30T12:30:00ZJSON output
Section titled “JSON output”--json prints an array with one object per thread: thread_id, sequence, state, status (the latest checkpoint’s status), suspended_node, next_node, waiting_at (the node path), metadata, updated_at, wait and graph. metadata holds every key, including the engine’s, such as a failed node’s error.
strata thread inspect
Section titled “strata thread inspect”strata thread inspect <THREAD_ID> --storage <STORAGE> [--blobs <DIR>] [--json]| Option | Value | Default | Meaning |
|---|---|---|---|
<THREAD_ID> |
A thread ID | Required | The thread to show. |
--storage |
A path or URL | Required | The store to read. |
--blobs |
A folder | The store’s database | Where offloaded values are. |
--json |
Off | Prints JSON. |
It shows the thread’s latest checkpoint in full, then its state.
strata thread inspect transfer-acme --storage checkpoints.sqliteThread: transfer-acme Status: interrupted Thread state: interrupted Current node: Approve Graph: wire-transfer version 3 Hops: 2 Waiting for: a human Times out at: 2026-10-31T09:00:02Z Metadata: tenant=acme Next node: Execute Checkpoint count: 3 Created at: 2026-10-30T09:00:02Z Resumed at: - State:{ "amount": 4200.5, "approved_by": null, "currency": "USD", "invoice": "INV-1042", "sent": false}Status is the latest checkpoint’s status. Thread state is the state thread list shows, which can be resumed. Some lines appear only when they apply:
| Line | When it appears |
|---|---|
Graph |
The graph was compiled with an identity. A thread stopped inside a subgraph also gets a Graph at <node> line for each child graph that has one. |
Lease |
A worker holds the thread’s lease: held by '<owner>' until <time> (token <n>). |
Running |
The thread is running: where its run is, or was when its process died, such as after 'Plan', next 'Execute'. |
Waiting at, Failed at, Cancelled at, Ended at |
The thread stopped inside a subgraph: the node path, such as Work/Approve. |
Recoveries |
The thread was recovered from this checkpoint. |
Reason |
Why the thread was cancelled or ended. |
Ended by |
Who ended the thread, when the end said. |
Hops |
The graph has a hop limit: how many nodes it has started. |
Waiting for, Wakes at, Times out at |
The thread is interrupted: what it waits for (a human, a timer, or event '<type>'), and when it wakes or times out. |
Decision |
A human decision is in effect, such as approved by alice at Approve, <time>. |
Metadata |
The run has metadata of its own. |
The Metadata line leaves out the engine’s keys (error, error_kind, attempts, cancel_reason and every key that starts with strata.). To see why a node failed, use --json: the error is in metadata.error.
JSON output
Section titled “JSON output”--json prints one object: thread_id, sequence, state, status, suspended_node, next_node, created_at, resumed_at, checkpoint_count, frozen_state, metadata, nested, decision, wait, graph and lease. lease is null, or an object with owner, token and expires_at.
{ "checkpoint_count": 2, "created_at": "2026-10-30T10:15:09Z", "metadata": { "attempts": "3", "error": "invoice service timed out", "error_kind": "transient", "strata.hops": "1", "tenant": "globex" }, "state": "failed", "status": "failed", "suspended_node": "Prepare", "thread_id": "transfer-globex"}This excerpt leaves out some fields.
strata checkpoint log
Section titled “strata checkpoint log”strata checkpoint log <THREAD_ID> --storage <STORAGE> [--blobs <DIR>] [--json]| Option | Value | Default | Meaning |
|---|---|---|---|
<THREAD_ID> |
A thread ID | Required | The thread. |
--storage |
A path or URL | Required | The store to read. |
--blobs |
A folder | The store’s database | Where offloaded values are. |
--json |
Off | Prints JSON. |
It prints one line per checkpoint, oldest first: the sequence, the status, the node (- before the first node, or the node path inside a subgraph), the next node, and when the checkpoint was written.
strata checkpoint log transfer-acme --storage checkpoints.sqlite#0 [running] node=- -> Prepare at=2026-10-30T09:00:00Z#1 [running] node=Prepare -> Approve at=2026-10-30T09:00:01Z#2 [interrupted] node=Approve -> Execute at=2026-10-30T09:00:02Z--json prints an array with one object per checkpoint: sequence, status, suspended_node, next_node, created_at and resumed_at.
strata checkpoint diff
Section titled “strata checkpoint diff”strata checkpoint diff <THREAD_ID> --seq-a <SEQ_A> --seq-b <SEQ_B> --storage <STORAGE> [--blobs <DIR>] [--json]| Option | Value | Default | Meaning |
|---|---|---|---|
<THREAD_ID> |
A thread ID | Required | The thread. |
--seq-a |
A sequence | Required | The first checkpoint. |
--seq-b |
A sequence | Required | The second checkpoint. |
--storage |
A path or URL | Required | The store to read. |
--blobs |
A folder | The store’s database | Where offloaded values are. |
--json |
Off | Prints JSON. |
It compares the state of two checkpoints, and prints each top-level field that changed, in key order. It compares top-level fields only: if one field inside a nested object changes, it prints the whole object, before and after. A field that exists on one side only gets one line. Get the sequences from checkpoint log.
strata checkpoint diff transfer-acme --seq-a 0 --seq-b 1 --storage checkpoints.sqlite--- Checkpoint #0 (Node: -)+++ Checkpoint #1 (Node: Prepare){- "invoice": null,+ "invoice": "INV-1042",}When no field changed, it prints (no state field changes).
--json prints thread_id, seq_a, seq_b, node_a, node_b and changes. Each change has key, before and after. A side that doesn’t have the field is null.
strata checkpoint replay
Section titled “strata checkpoint replay”strata checkpoint replay <THREAD_ID> --sequence <SEQUENCE> --storage <STORAGE> [--blobs <DIR>] [--json]| Option | Value | Default | Meaning |
|---|---|---|---|
<THREAD_ID> |
A thread ID | Required | The thread. |
--sequence |
A sequence | Required | The checkpoint. |
--storage |
A path or URL | Required | The store to read. |
--blobs |
A folder | The store’s database | Where offloaded values are. |
--json |
Off | Prints JSON. |
It shows what a resume from that checkpoint would start from: the node the thread stopped at, the node path inside a subgraph, the human decision in effect, the node it would resume at, and the state. It doesn’t run the node. Your nodes are compiled into your application, not into the CLI.
strata checkpoint replay transfer-acme --sequence 2 --storage checkpoints.sqliteLoading historical state from Checkpoint #2... Done.Suspended at node: ApproveWould resume at node: ExecuteState at this checkpoint:{ "amount": 4200.5, "approved_by": null, "currency": "USD", "invoice": "INV-1042", "sent": false}--json prints thread_id, sequence, status, suspended_node, next_node, state, nested and decision.
What the CLI doesn’t do
Section titled “What the CLI doesn’t do”- It doesn’t change threads. There is no command to resume, signal or end a thread. Do that from your application.
checkpoint replaydoesn’t run anything. It shows the state and the next node, but doesn’t run the node or swap its model.thread inspectdoesn’t show token usage or a summary of the conversation history.- There are no ARM64 builds. The release has x86-64 binaries for Linux, macOS and Windows. On other machines, build it from source.
Known limitations lists what to do in the meantime.