State, storage and threads

Four words come back on every page: state, checkpoint, thread and storage. This page says what each one is and how they fit, so the guides can be short.

State#

Every run has a state: a JSON object with string keys.

  • In a workflow, you choose the keys. Nodes read the state and return the keys they change; the engine merges the changes after each step.
  • In an agent, the engine keeps the transcript under messages and its own counters under a reserved key. Values you pass with a message (send(text, user_id="42") in Python, { state: { user_id: "42" } } in TypeScript) are added to the state: memory and hooks read them.

The state is always plain JSON, so it can be saved, sent and inspected in any language.

Checkpoint#

A checkpoint is the saved state of a run at one moment, with what is needed to continue: which step comes next, which tool calls have finished, whether the run is paused and why. CriogenAI writes one:

  • when a message arrives (source: "input"),
  • after every step and every finished tool call ("step"),
  • when the run pauses for a person ("suspend"),
  • when it fails ("failure"),
  • when you rewind ("fork").

Each checkpoint has an id, the run_id it belongs to, a status (in_progress, suspended, completed, failed) and a version that grows by one with every write. history() lists them, newest first.

Thread#

A thread is a named line of checkpoints: one conversation with an agent, or one durable run history of a workflow. Its newest checkpoint is the head; the next message continues from it.

thread "ticket-7"
  v1 input → v2 step → v3 step → v4 suspend      ← head: waiting for approval
                                   approve()
                                 → v5 step → v6 step (completed)   ← new head

You never create a thread explicitly: the first send (or run, for a workflow) on a new id creates it. A thread's id is yours to choose; use an opaque id (a UUID, a ticket number) rather than a person's name, because thread ids are stored in clear text even in an encrypted database.

What the head's status means for the next call:

Headsend (agents) / run (workflows)What to call
completedStarts a new turnanything
suspendedFails: thread_suspendedapprove, reject, decide (agents), resume (workflows)
failedStarts a new turn; the failed one is left as it wasretry() continues the failed turn instead
in_progress with no run alive (the process died)Fails: thread_interruptedretry()

Only one run uses a thread at a time. A second call on a busy thread waits by default; see Storage.

Storage#

Storage is the database behind threads. One Storage keeps:

BackendURLUse for
In memorynoneTests, scripts. Gone when the process ends
SQLitesqlite://path/to/file.dbOne process on one machine
Postgrespostgres://user:password@host/dbSeveral processes or machines

An agent without storage gets its own in-memory storage: threads work, but only until the process ends, and only within that agent object. Agents (and workflows) built with the same Storage share its threads and memory, so a second process with the same database can answer an approval the first one is waiting for.

Snapshot#

A snapshot is a checkpoint you hold yourself instead of storage: plain JSON returned by a workflow that paused or failed while running without storage (result.snapshot, error.snapshot). Pass it back to resume to continue. Use threads instead whenever you can: they save and load snapshots for you.

Putting it together#

You wantUse
One answer, no historyagent.run(text)
A conversation that continues lateragent.thread(id).send(text) with a Storage
A person to approve a tool call, maybe days laterapprove=[...] and a SQLite or Postgres Storage
Facts about a person in every conversationmemory=Memory.by_state(...)
A fixed series of steps that may pauseA Workflow compiled with a Storage, and app.thread(id)
To go back to an earlier pointhistory() and rewind(checkpoint_id)

Next#