Skip to content
These docs cover Strata 0.9.0-preview.1.It is a preview, and the API can still change before 1.0.

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.

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.
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.

--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 list prints No 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.

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:

Terminal window
strata thread inspect transfer-acme --storage checkpoints.sqlite --blobs ./blobs

thread list doesn’t take --blobs, because it doesn’t print state.

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 --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. --json prints them.
  • The graph’s ID and version on a Graph: line at the top, when the definition has an identity.
Terminal window
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 ends
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.

Terminal window
strata graph validate --path graphs/billing.json
FAILED: '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.

Terminal window
strata graph validate --path graphs
OK graphs/approval.json
FAILED 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.json
ERROR failed to parse 'graphs/notes.json' as a graph definition: missing field `schema_version` at line 1 column 7
4 graph file(s) checked: 2 valid, 1 invalid, 1 unreadable.

This run exits with 2: an unreadable file outranks a graph with errors.

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.

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 graphs

The 14-graph-ci-gate demo runs the whole gate, in C# and Rust.

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.

Terminal window
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:

Terminal window
# Failed threads of one tenant
strata thread list --storage checkpoints.sqlite --status failed --meta tenant=globex
# Threads of version 3 of the wire-transfer graph
strata thread list --storage checkpoints.sqlite --graph wire-transfer --graph-version 3
# Waiting threads whose timer or timeout is due by 12:30 UTC
strata thread list --storage checkpoints.sqlite --due-by 2026-10-30T12:30:00Z

--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 <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.

Terminal window
strata thread inspect transfer-acme --storage checkpoints.sqlite
Thread: 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 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 <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.

Terminal window
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 <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.

Terminal window
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 <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.

Terminal window
strata checkpoint replay transfer-acme --sequence 2 --storage checkpoints.sqlite
Loading historical state from Checkpoint #2... Done.
Suspended at node: Approve
Would resume at node: Execute
State 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.

  • It doesn’t change threads. There is no command to resume, signal or end a thread. Do that from your application.
  • checkpoint replay doesn’t run anything. It shows the state and the next node, but doesn’t run the node or swap its model.
  • thread inspect doesn’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.