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.

State

State is the one typed value that flows through a graph. Every node receives the current state and returns a new one. When a thread stops, Strata saves the state in a checkpoint, and a later resume reads it back into the same type.

This page explains what the state type must be, how Strata stores it, and what belongs in it.

You choose the type. In C# it is usually a record. In Rust it is a struct. The graph is generic over it: StateGraph<TState> in C#, StateGraph::<S> in Rust.

C# Rust
Usual shape A sealed record with init-only properties A struct
Required to build a graph Nothing Clone + Send + Sync + 'static
Required to run it Must round-trip through System.Text.Json (not checked when you compile) Serialize + DeserializeOwned as well
Serializer System.Text.Json, with its default options serde_json
JSON field names by default The property names as you wrote them (ApprovedBy) The field names as you wrote them (approved_by)

The type must serialize to a JSON object, because a checkpoint stores the state as one. A record or a struct does.

In C#, the engine passes no JsonSerializerOptions, so the defaults apply. Public properties are written, fields are not, and names are matched case-sensitively on the way back in. A positional record works as it is. If your type needs anything special, use System.Text.Json attributes on the type itself.

In Rust, the engine clones the committed state into each attempt and runs handlers on other tasks, which is why the type must be Clone, Send and Sync. #[derive(Clone, Serialize, Deserialize)] is all most state types need.

A node doesn’t change the state it receives. It returns the next one. In C# you copy the record with with. In Rust you build a new struct with ..state for the fields you keep. The Quickstart shows both.

Because each node returns a whole value, the state after a node is always complete and consistent. That is the value Strata commits, routes on with conditional edges, and saves in a checkpoint.

A checkpoint holds the state as JSON, in its frozen_state field. The rest of the checkpoint (status, next node, timestamps and so on) has a fixed format, defined by schema/checkpoint.schema.json. The state is yours, so the schema doesn’t describe it further.

When a thread resumes, Strata reads frozen_state back into your type. If the JSON no longer fits the type, the resume returns Failed with the message failed to rehydrate state for thread '<id>', and no node runs.

A thread can wait for days, so a newer build of your application may resume a checkpoint that an older build wrote. Change the state type so it still reads old JSON. Adding a field is safe when it can be missing: System.Text.Json leaves a missing property at its default, and serde accepts a missing Option field, or any field marked #[serde(default)]. Renaming or removing a field, or changing its type, breaks threads that are already saved.

The two engines write the same checkpoint format, so a thread that one engine paused can be resumed by the other. The checkpoint’s own fields always agree. The state inside it agrees only if both of your state types serialize to the same JSON.

By default they don’t: C# writes ApprovedBy, and Rust looks for approved_by. The convention, from schema/README.md, is snake_case. Pin each C# property’s name with [JsonPropertyName], and leave the Rust struct as it is:

using System.Text.Json.Serialization;
sealed record WireTransferState(
[property: JsonPropertyName("invoice_generated")] bool InvoiceGenerated,
[property: JsonPropertyName("amount")] double Amount,
[property: JsonPropertyName("currency")] string Currency,
[property: JsonPropertyName("approved_by")] string? ApprovedBy);
// A node returns a copy with the fields it changes.
.AddNode("PrepareWireTransfer", state => state with { InvoiceGenerated = true })

Names are the usual difference, but not the only one. Check that every field’s value is written the same way in both languages. For example, System.Text.Json writes an enum as a number by default, and serde writes it as the variant’s name.

Nothing in Strata compares the two types for you. The two halves of demo 09 define the same state independently and share only the database file.

A parallel group runs several branches at once, each starting from the same state. A map runs a child graph once per item of a list. In both cases several states come back, and the graph needs one.

You merge them with a reducer: a function you pass to AddParallelGroup / add_parallel_group or AddMap / add_map. It receives the state from before the group and the results, and returns the state that flows on. Results arrive in a fixed order, the branches’ declaration order or the map’s list order, never the order they finished. So the merge is deterministic.

The reducer is one function over whole states. Strata has no per-field reducers that declare how each field merges. The graphs page explains groups and maps.

When a person answers an interrupt boundary, the answer can carry field overrides. Strata writes each one into the saved JSON before it reads the state back, replacing a top-level field. An override is keyed by the field’s JSON name: ApprovedBy for a plain C# record, approved_by for a Rust struct or a record that pins snake_case. See Human-in-the-loop.

Strata serializes the whole state at every checkpoint. With the EveryNode checkpoint policy, that is after every node that commits. In Rust the engine also clones the state into every async handler. The benchmark results measure a 16 KiB state adding 54% to a ten-node Rust run, and nothing in .NET. Small state keeps both costs low.

Keep secrets out of state. Checkpoints are stored as plain JSON, and Strata has no field-level encryption (see Known limitations). Keep the secret in your secret store and put a reference to it in the state.

Hold large values by reference where you can. A document, a search result set or gathered evidence can live in your own storage, with only its ID in the state.

When a large value must stay in the state, the SQLite, Postgres and Redis checkpoint stores can store it more cheaply. Both options are off by default, and you turn them on in the store’s options:

Option What it does C# Rust
Delta checkpoints Stores a running checkpoint as the top-level fields that changed or went since the thread’s previous one. At least one row in 16 is stored whole, and so is every checkpoint that isn’t running. Deltas = new DeltaOptions() .with_deltas(Deltas::new())
Value offload Stores each top-level field whose JSON is 64 KiB or more once, in a blob store, and keeps a reference in the row. The blob store is the checkpoint store’s own database by default. Offload = new OffloadOptions() .with_offload(Offload::new())

Both work inside the store. The engine and your nodes still see the whole state, and every reader decodes the stored form. Upgrade every reader, the strata CLI included, before a writer turns either option on. The format details are in schema/README.md.

In the benchmark results, a state with a 64 KiB value that never changes stored 65.6 KB per save whole, 6.6 KB on average with deltas, and 3.4 to 3.7 KB with the value offloaded. Loading reads more: a chain of deltas, or the blob.

State is one of three places a graph keeps data. Each has its own store.

What belongs there Scope Store
State What the next nodes need to decide and act: inputs, decisions, small results, references One thread A checkpoint store
History A conversation’s messages, by role and text One session, named by a session ID you choose A history store (IHistoryStore / HistoryStore)
Long-term memory Records an agent recalls in later runs by similarity, such as past incidents Across runs and threads A memory store (IMemoryStore / MemoryStore)

A conversation doesn’t belong in state. Nodes read and append it through the history store, so a checkpoint doesn’t carry every message.

Each demo exists in both languages, with the same folder name:

  • 01-hello-agent: the smallest typed state. C#, Rust.
  • 04-multi-node-graph: a state that several nodes update in turn. C#, Rust.
  • 09-cross-language-interop: one engine pauses a thread, the other resumes it, with snake_case state and an override keyed by its JSON name. C#, Rust.
  • 10-parallel-fan-out: a parallel group and its reducer. C#, Rust.