Skip to main content
The store is Aether’s single source of truth. Every phase transition, input resolution, output merge, and timeout deadline is written through store.Store. The engine never caches state locally — it reads what it needs, mutates through the store, and trusts that the store’s version of reality is authoritative. This makes the engine restartable and, in distributed deployments, replaceable behind any consistent backend.

The Store interface

store.Store is an aggregated interface that composes four focused sub-interfaces plus a lifecycle method:
You must implement all four sub-interfaces. Partial implementations will fail to compile.

Sentinel errors

Two sentinel errors signal expected, non-exceptional conditions:
Return errors that wrap these values (using fmt.Errorf("...: %w", store.ErrNotFound)) so callers can use errors.Is to distinguish them from infrastructure failures.

Optimistic concurrency: the Token model

All mutable records carry an opaque uint64 field called Token. The framework passes the token it read from the last Get call into every subsequent Update call unchanged. The store implementation decides what this means:
  • A monotonic counter (e.g. increment on every write) gives strict optimistic locking — mismatched tokens return ErrTokenMismatch.
  • A no-op (always accept) is valid for single-threaded or test implementations.
MemoryStore uses a monotonic counter, incrementing Token on every successful update and returning ErrTokenMismatch if the token passed by the caller does not match the current stored value.
The token is opaque to the engine: it never interprets the numeric value. Your implementation defines the semantics — the engine only guarantees that it echoes back whatever token it last received.

WorkflowRunStore

Manages the lifecycle of workflow run records.
method
Stores the initial workflow run record. RunID must be unique; duplicates should return an error. Workflow (the raw JSON) and CreatedAt are set here and never modified again.
method
Retrieves a workflow run by ID. Returns ErrNotFound (wrapped) if missing. Return a copy — the caller may mutate the returned struct.
method
Persists mutable fields. Only non-nil pointer fields in run are written; nil fields are left unchanged (nil-field partial update convention). Must check run.Token against the stored token and return ErrTokenMismatch on mismatch. Returns the post-update state.
method
Returns all non-terminal workflow runs that have a Deadline set. Used by the timeout watchdog to detect overdue workflows.
method
Removes a workflow run and all its associated TaskRun records. Returns ErrNotFound if the run does not exist. Implementations must cascade-delete child task runs.
method
Returns all WorkflowRun records created by the given CronWorkflow. Used by the cron controller for concurrency policy checks and history cleanup.

The WorkflowRun persistent model

TaskRunStore

Manages individual task execution records.

CreateTaskRun idempotency contract

CreateTaskRun is idempotent by composite key: (workflowRunID, parentRunID, scope, taskName). If a task run with the same four-field key already exists, the implementation must return nil rather than an error or a duplicate. The engine relies on this contract for concurrent-safe scheduling — the scheduler may attempt to create the same task run multiple times under concurrent completion events.
Violating the idempotency contract — returning an error or inserting a duplicate on a repeated CreateTaskRun call — will cause the engine to double-dispatch tasks or stall a workflow run permanently. This is the most critical correctness requirement of the TaskRunStore interface.
The Scope field is part of the key because loop iterations share the same TaskName (the body template) but each receives a unique scope string (e.g. "poll-job.loop[0]/", "poll-job.loop[1]/").
method
Returns task runs sharing the same parent container. parentRunID="" returns top-level tasks. This is the core scheduling query: advanceScope calls it to enumerate siblings within a single DAG or loop scope.
method
Returns all non-terminal task runs (including pending ones) that have a Deadline set. Pending tasks are included because the deadline is written at dispatch time, before OnTaskStarted transitions the run to Running.

The TaskRun persistent model

TaskRun records form a parent-child tree via ParentRunID that mirrors the template nesting structure. This tree enables scoped scheduling (advanceScope only looks at sibling TaskRuns), variable isolation (each scope sees only its own sibling outputs), and upward propagation (when all children of a container complete, the container is finalized).

SchemaStore

Persists executor schemas for crash-recovery and distributed schema propagation.
UpsertSchema inserts or overwrites a schema. workerID="" denotes an orphan record loaded during recovery. DeleteSchema accepts optional execType and workerID filters (empty = no restriction), combined with AND semantics — allowing GC eviction of stale executor types or cleanup when a worker goes offline.

CronWorkflowStore

Manages persistent records for scheduled (cron-based) workflows.
ListCronWorkflows is called during engine.Start() to re-register all cron schedules after a crash or restart.

The nil-field partial update convention

Both UpdateWorkflowRun and UpdateTaskRun apply a nil-means-skip rule: only pointer fields that are non-nil in the update struct are written to the store. Nil fields are left unchanged. This avoids accidental overwrites when only a subset of mutable fields need updating. Example (from MemoryStore):

Engine option

WithStore is required. The engine will panic at New() time if no store is provided.

Reference implementation: MemoryStore

The playground MemoryStore is a fully compliant, thread-safe in-memory implementation. It demonstrates every contract described above and is suitable for unit tests and single-process deployments.
Key implementation details visible in MemoryStore:
  • Token is incremented on every successful Update call (existing.Token++).
  • UpdateWorkflowRun and UpdateTaskRun check existing.Token != run.Token and return store.ErrTokenMismatch on mismatch.
  • CreateTaskRun calls taskExistsLocked to check the composite key before inserting, returning nil (no error) on a duplicate.
  • All Get methods return copies of stored structs, including deep copies of pointer fields, to prevent callers from mutating internal state.
  • DeleteWorkflowRun cascade-deletes all TaskRun records for the workflow run from both taskRuns, taskIndex, and parentIndex.
MemoryStore also records a full-state Snapshot after every mutating operation, enabling the playground’s HTML report to replay store history step-by-step — a useful pattern for debugging and audit logging.