Prepare the graph
Run the coordinator
Register the coordinator on workers serving its queue, alongside every application task handler. It uses bounded point reads through the inspection capability.Retention during long workflows
The builder raises every child retention value to at least the workflow retention. On each tick, the coordinator copies newly observed completions into its fenced checkpoint before it promotes dependent work. This makes completion evidence durable even when an early job row expires while a later task is still running or retrying. If operators must retain every child’s full job detail, choose a workflow retention that covers the expected maximum workflow runtime plus the desired inspection window. Dependency correctness no longer depends on keeping every completed row for that entire time. A missing node without recorded completion evidence remains a workflow failure.Long-running tasks and lease loss
The runner renews leases while a handler is executing. A suspended laptop, stopped container, network partition, or overloaded process cannot renew, so the store eventually reclaims the expired lease and counts a crash. That is expected recovery behavior: the old attempt is fenced and a later attempt owns the job. Make long stages resumable instead of relying on one uninterrupted process lifetime. Use a named cursor step, persist the cursor after each safely repeatable unit, and stop promptly when the handler context is cancelled.Inspect graph state
Graph reads are available directly from both workflow libraries; they do not depend on the console. A snapshot includes the accepted base graph plus additive grafts, current node and coordinator states, revision, retry generation, dependency edges, reverse dependent edges, and virtual-node configuration. Application payloads are excluded.workflow_node / GetWorkflowNode, workflow_dependencies /
WorkflowDependencies, and workflow_dependents / WorkflowDependents for convenient
single-relation reads. When several questions are needed, fetch one snapshot and use its
in-memory methods so the graph is internally consistent and the store is read only once.
The same reads are available over HTTP. GET /api/v1/workflows returns a bounded,
cursor-paginated coordinator list; GET /api/v1/workflows/{id} and the
/nodes/{node}, /dependencies, and /dependents subresources. A retained completion
remains completed after its job row expires; an unrecorded absent row is explicitly
reported as missing.
Durable signals, payloads, and source history
A signal is declared as a workflow node and stored as a retained pending internal job, so delivery survives worker restarts on PostgreSQL, Redis, and MySQL. Emitting beforeprepare completes is safe: Headgate records the emission first and releases the signal
job second. The coordinator consumes the completed signal only after its dependencies are
complete.
Use the rich emission API when the event carries business data. payload is the fact the
workflow is waiting for; source describes who or what emitted it. Both are arbitrary JSON
values. The Idempotency-Key (or SDK idempotency_key) identifies one emission: replaying
the same key and content returns the original record, while reusing the key with different
content is rejected.
source is caller-supplied metadata, not proof of identity—
authenticate the control API upstream and populate it from that trusted identity rather
than accepting arbitrary end-user values. The console shows matching signal history when
you inspect a signal node or a task directly waiting on one.
The short emit_signal / EmitSignal helpers remain for name-only events. They store a
null payload and empty source object, and use one legacy key per signal name; use the rich
API whenever multiple emissions or audit context matter.
Absolute timers use the backend’s store clock and scheduled-job promoter:
finalized_at_ms. The coordinator durably records that evidence
before atomically changing the timer from pending to scheduled at anchor + delay, so
worker clock skew and coordinator polling latency do not move the deadline.
CEL waits
Use a condition node for a bounded boolean decision over workflow state. Available variables arerevision and generation (unsigned integers), states (node name to
state), and completed (node name to boolean).
prepare, cannot perform I/O, and
must return a boolean.
Experimental graph grafts
Add ordinary tasks to a workflow that is still running by preparing a batch against the current graph revision. Enqueue the returned receipt and tasks together in one atomic store call.1 is the initial graph, so this batch requests revision 2. The receipt id is
deterministic (onboarding:42:graft:2): concurrent writers using the same expected
revision collide at atomic enqueue instead of both changing the graph. The coordinator
validates the combined DAG, checkpoints the new revision and nodes, and only then releases
the receipt handler. A crash in between replays the same accepted receipt.
Grafts are additive and currently accept ordinary tasks only. They cannot replace or
delete an existing node or revive a completed coordinator. Accepted grafts enter the
bounded workflow history. POST /api/v1/workflows/{id}/grafts provides the authenticated,
idempotency-key-protected control route. The console remains read-only while its mutation
controls are reviewed.
Non-task nodes cannot be grafted
Signals, absolute or relative timers, CEL conditions, and child-workflow links must be declared when the initial workflow is prepared. NeitherWorkflowGraft::add /
WorkflowGraft.Add nor POST /api/v1/workflows/{id}/grafts accepts those node types.
Attempting to represent one as an ordinary task does not give it signal, timer,
condition, or child-workflow semantics.
This boundary is deliberate. An ordinary task graft only adds a pending job and dependency
edges. Each non-task node needs additional durable rules:
- a signal needs a stable buffered-delivery identity and idempotent emission lookup;
- a relative timer needs a store-stamped dependency-completion anchor;
- a condition must be compiled, bounded, and evaluated against a defined workflow state;
- a child link changes the cross-workflow graph and therefore needs cycle detection, creation atomicity, cancellation propagation, and retry propagation.
An accepted node and its dependency edges are immutable, including while the workflow is
active. A graft can append new ordinary tasks that depend on existing nodes, but cannot
replace, rename, remove, or rewire accepted work. Use a new workflow ID when a change
requires a different interpretation of the existing graph.
Experimental failed-subgraph retry
Enable retry before the workflow is enqueued. This retains dependency-blocked pending jobs when a node exhausts rather than deleting them.automatic_retry(max_generations, backoff) in Rust or
AutomaticRetry(maxGenerations, backoff) in Go. The generation limit includes the first
run and the backoff uses the ordinary store-timed snooze path.
POST /api/v1/workflows/{id}/retry can also repair states that are unsafe to reopen
blindly. A quarantined node requires release_quarantine: true (the release applies to its
fingerprint). An undecodable node requires a replacement base64 payload and positive
schema_version. Completed ancestors remain untouched. A failed child link requests the
child coordinator’s failed-subgraph retry before the parent link is reopened.
Experimental child workflows
Create the child and parent builders, link the child coordinator as a parent node, and use one atomic bundle:register_coordinator / RegisterCoordinator installs the coordinator and all internal
workflow handlers. The mutation API requires Idempotency-Key and uses the same upstream
authentication boundary as the rest of the control API.
The experiment now has durable signals, dependency-anchored timers, CEL waits, additive
grafts, atomic child bundles, retry/repair, cancellation propagation, authenticated API
routes, and a bounded 256-event workflow history. The console remains intentionally
read-only while its mutation controls are reviewed.