docs/architecture/chasm.md
This document is a step-by-step introduction to the core architecture and domain entities of the CHASM framework.
Temporal Workflows are powerful, but they have real limits: too slow or heavyweight for some problems, unable to scale in every dimension (e.g. millions of signals, large payloads), and overly complex when a purpose-built solution would be simpler.
CHASM addresses this by treating Workflow as just one Application State Machine (ASM) among many. An ASM is a specialized state machine that leverages Temporal infrastructure like sharding, routing, atomic storage, failure recovery without the full cost of a Workflow — and hides those distributed systems details behind a clean, typed API so developers can focus on business logic.
An ASM is a registered state machine type, composed of a Library, Component types, and Tasks.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">The global catalog of all registered Libraries.
A Library groups components, tasks, and service handlers into a namespace.
Examples: workflow, scheduler and nexusoperation
A registered type that defines state (Fields) and behavior. Each Component type is identified by a name (aka Archetype), which CHASM converts to a stable ID (aka Archetype ID) for storage.
[!NOTE] At runtime, a Component type is instantiated as a Component living inside a Node — see Execution.
Asynchronous work units (e.g. network calls, timers) that a Component type can schedule. Registered in Library alongside Component types.
The framework-managed containers for a component's persisted state.
</td> </tr> </table>A Component's state is made up of typed Fields. CHASM uses these to persist and replicate data.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">Field[T] — stores a single value.Map[K, T] — stores a keyed collection.ParentPtr — a typed reference to the parent Component. Allows a child to read its parent's state and call its methods.Field) that hold in-memory derived state. Not persisted; invalidated and recomputed as needed.T determines the field kind:
T is a Protobuf message; stores serialized data.T is a child Component; stores a nested component subtree.</td> </tr> </table>[!NOTE]
FieldandMapchildren are each persisted as separate nodes. Use a separate field for data that changes at a different frequency, is large, or is only read in certain operations.
An Execution is a runtime instance of an ASM. Namespace retention, visibility records, and ID reuse policies all apply at the Execution level.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">The unique identifier of an Execution, composed of:
NamespaceID
BusinessID — user-defined name (e.g., a Workflow ID). Persists across resets.
RunID — a single instance. Changes on reset or when a follow-up run is started under the same BusinessID; the BusinessID stays the same.
</td> </tr>The state of the Execution where ExecutionKey identifies the Root Component.
Map key.The root Node and its descendants form the CHASM Tree of the Execution.
The sequence of Names from the root to a child Node within an Execution (e.g. ["callbacks", "cb-1"]).
Every Component implements a lifecycle method that CHASM uses to determine whether it has reached a terminal state.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">When the Root Component reaches a terminal state, the Execution closes: its BusinessID becomes available for reuse and it is eventually deleted according to the namespace's retention policy.
</td> </tr> </table>The Engine is the entry point for interacting with Executions.
<table> <tr style="background-color: transparent"> <td width="600"> </td> <td width="400">PollComponent callers waiting on the Execution.Read-only. Provided during reads and task validation.
</td> </tr> </table> <table> <tr style="background-color: transparent"> <td width="600"> </td> <td width="400">Provided during write operations. Allows writing Fields and scheduling Tasks.
</td> </tr> </table>A Transition is the atomic unit of state change in CHASM. It operates on the entire Execution — any Node can be read or mutated.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">Any external trigger delivered to a Component — e.g. a network callback, timer firing, or incoming signal.
Developer-provided code that runs when the Event is delivered. May write Fields and schedule Tasks.
All Field writes and Task schedules from a transition commit together as a single database write, or roll back entirely. On commit, every modified Node is stamped with a new VersionedTransition.
</td> </tr> </table>Transitions can schedule Tasks for deferred work. There are two kinds, differing in when they run and what they can access. Tasks are written atomically with the component state (transactional outbox pattern), guaranteeing no work is silently lost on crash.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">Runs within a transaction with full read/write access to component state. A Pure Task scheduled without a deadline runs immediately in the same transaction as the scheduling Transition. One scheduled with a deadline runs in a new transaction when the deadline fires.
Runs asynchronously after the transaction commits. Can call external services but cannot mutate state directly — any state change must go through the same API as any external caller.
Each task type is processed by a handler with two methods:
If the Validator or Executor returns an error, the framework retries the task until it succeeds or the Validator discards it.
Tasks are processed in FIFO order in general, but strict ordering is not guaranteed.
</td> </tr> </table>Every transition is stamped with a VersionedTransition — the logical clock of CHASM.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">Provides a total ordering of all state changes, even across data centers.
FailoverVersion — increments when the owning cluster changes (cross-DC failover).
TransitionCount — increments with every state update within the Execution.
</td> </tr>When a component starts an external task and needs the result routed back as a callback, it creates a ComponentRef — a serialized token that acts as a return address.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">Contains the full ExecutionKey and ComponentPath so the callback can find the right node.
Also carries two VersionedTransition values:
When a callback arrives, the Engine loads the Node at the given ComponentPath and verifies both VT values before allowing the transition to proceed. A mismatch on either value causes the callback to be rejected.
</td> </tr> </table>Visibility is a built-in child Component that manages an Execution's visibility record.
<table> <tr style="background-color: transparent"> <td width="500"> </td> <td width="500">Indexed key-value pairs that make Executions queryable. There are two sources:
Unindexed key-value metadata attached to an Execution. There are two sources:
User-defined custom Memo stored by the archetype user.
Component-defined Memo computed dynamically from the root component's state, provided by implementing the memo provider interface.
</td> </tr>Each ASM is a self-contained package.
my_asm/
├── proto/
│ └── my_asm.proto # Service API (gRPC) and state/field message types
├── <component>.go # Component struct, Field declarations, lifecycle method etc.
├── config.go # Dynamic config settings for the ASM
├── frontend.go # Frontend gRPC handler to map API calls to Engine operations
├── fx.go # fx module to wire the Library into the application
├── library.go # Registers component and task types with the Library
└── statemachine.go # (optional) Transition declarations using the statemachine framework