A workflow's `preferred_targets` entry can be written `member@{rig}`. When an instance is bound to a rig (`--rig`, or a registered spec default), the trailing `{rig}` resolves to that rig, so one cached spec routes each bound instance's work to its own rig through later hops, failure and exception packets, ordinary and occurrence-local resume, and a database reopen. The cached spec itself stays literal.
- Only a trailing `@{rig}` is replaced, through one helper shared by binding, the validator and exception readiness. Braces in the member part or a partial rig name stay literal.
- Without a binding, the target stays literal and is handled by the normal resolution and queue admission at the step that selects it. Starts whose entry resolves still start, including ones with `{rig}` only in a later or unused role or a second preferred target, and an explicit entry owner keeps its precedence. An unbound placeholder-only start keeps main's 400 `unknown_destination_rig` with the instance and packet rolled back, and an unresolved later hop fails at that hop with the same 400.
- Unbound `validate` reads a `{rig}` target as `unregistered`, saying it needs a bound rig or a literal rig name. Once bound, the resolved target uses the existing readiness check.
- The slice view shows a bound instance's `{rig}` target as the seat it routed to.
20 KiB
kind, title, status, topics, domains, applies-when, last-verified-against-source, last-updated
| kind | title | status | topics | domains | applies-when | last-verified-against-source | last-updated | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| as-built | Workflow Runtime — Specs, Packets, and Lifecycle Graphs | active |
|
|
Following workflow validation, instantiation, projection, routing, failure recovery, lifecycle graph revisions, or the corresponding CLI/API. | e8f0ab340d |
2026-10-08 |
Workflow Runtime — Specs, Packets, and Lifecycle Graphs
Source snapshot: e8f0ab340db773392ec8be75b072d1c0f3068a50. This describes the source at that commit;
it does not establish the version or behavior of a running daemon.
A workflow binds a specification to durable queue packets. The daemon records the instance, its current frontier, and the step trail; agents supply step outcomes and evidence. Serial workflows and dependency graphs share this runtime. The engine checks declared fields and recorded state; it does not judge the substance of a referenced proof document.
Source map
All paths in this table are under packages/daemon/src/.
| File / symbol | Responsibility |
|---|---|
domain/workflow-types.ts |
Spec, instance, trail, frontier, and failure-occurrence contracts. |
WorkflowSpecCache |
YAML parsing, source-hash cache, stored specs, diagnostics and retention. |
WorkflowValidator |
Roles, exits, routing/dependency edges, pins, cycles, and advisories. |
WorkflowRuntime |
Instantiation, inspection, routing, resume, abort, and lifecycle facade. |
WorkflowProjector |
Close the current step and update its successor/frontier state. |
WorkflowInstanceStore |
Instances, guarded updates, frontier bindings, and failure occurrences. |
WorkflowStepTrailLog |
Append/read step history. |
workflow-reconciliation.ts |
Inspect graph changes, apply explicit revisions, recover operation receipts. |
workflow-keepalive.ts |
Watchdog evaluation and reminders. |
Specification and cache
parseWorkflowSpec parses the workflow document and checks known key sets.
WorkflowValidator.validate then checks relationships between roles, steps,
exits, and targets. The exits are handoff, waiting, done, and failed.
An exit-specific next_hop.on mapping takes precedence over structural
routing; without a mapping, resolveNextStep considers forbid, suggested
roles, require, and then the next array element.
The language includes harness, host, singular gate, depends_on, typed
acceptance, and waiting re-presentation settings. At this pin,
WORKFLOW_AGENT_HARNESSES contains only claude-code and codex; support for
another runtime elsewhere in OpenRig does not add it to this pin's workflow
harness grammar. Nonlocal host pins can validate as registered names but
instantiate still rejects execution with host_pin_remote_unsupported.
This is a workflow-step restriction, not a claim that all queue traffic is
local-only.
A step whose allowed_exits can neither finish it nor route it on is refused as
step_cannot_finish (workflow-validator.ts:223-236): its exits leave out done, none is
mapped in next_hop.on, and it allows no handoff that routes on. In a dependency graph,
allowing handoff is enough, because handoff completes a sink. instantiate refuses any spec
with a validation error.
allowed_exits and max_hops affect validation/projection. In contrast,
continuation_required, preserve_lineage, closure_required, closure
messages, skill_refs, and spawn_budget produce
declared_not_enforced_v1 advisories. Legacy gates: [] and
next_hop.mode: prefer are rejected by the parser rather than silently
accepted. Prerequisite cycles are rejected; a routing cycle requires a
max_hops declaration and is distinct from a prerequisite cycle.
re_present_after_seconds compiles to the queue's park timer on an unrouted
waiting exit, and re_present_max_seconds turns it into a repeating
reminder with backoff (workflow-projector.ts:534–549); a step that cannot
reach waiting, or that maps waiting to a branch, fails validation
(waiting_re_presentation_unreachable).
WorkflowSpecCache.readThrough parses the source file, hashes it, and
updates the (name, version) cache row. Full spec_json preserves fields
that the older scalar columns cannot represent. A same-name/same-version
source edit can update that cache row; the version label alone is not an
immutable content pin. getByNameVersion is the runtime lookup.
Diagnostic-only rows, moved sources, and versions retained for unfinished
work have separate paths in readThrough, writeDiagnostic, and
isPinnedByUnfinishedWork.
Durable state
The migration files record the storage evolution; they are not a count of all database migrations:
| Migration | Workflow state introduced |
|---|---|
033, 034, 035 |
Spec cache, instances/frontier/current step, and append-only trail API. |
040, 050 |
Cache diagnostics and complete parsed spec JSON. |
049, 051, 052 |
Optimistic version guard, resume/hop baseline, and bound rig. |
079 |
Lifecycle operation key/digest/binding, packet-to-step frontier bindings, and failure occurrences. |
Current instance states include active, waiting, completed, failed,
and aborted. A multi-packet frontier needs packet-to-step bindings; a
single current_step_id cannot describe all parallel work.
Instantiation and owner selection
WorkflowRuntime.instantiate resolves a cached spec name or explicit path,
validates it, resolves the rig and the entry owner, then creates the instance
and its single entry packet (for steps[0]) through the transactional
event/queue path (workflow-runtime.ts:433, :757). The entry packet gets a
best-effort post-commit nudge with no staged wake intent (:843–845); its
keepalive, armed in the same transaction, and the boot sweep cover a lost
entry nudge. An explicit
unknown targetRig fails; an unknown spec-default rig produces an advisory
and falls back to unbound operation. A bound rig without a required declared
role is a different error.
resolveDefaultOwner uses declared preferred_targets first, applying a
harness match when pinned. With no preferred targets and a bound rig, it uses
selectRoleSeat:
a running managed agent with the declared role and required runtime, ordered
by pending-only backlog and then canonical coordinate. Infrastructure and
unresolvable/adopted coordinates do not become implicit replacement agents.
Explicit owner overrides are reconciled with harness pins. Declared
preferred_targets are not checked for liveness; validate and instantiate
only warn (role_no_live_preferred_target) when none of a role's targets has
a running session. A structured gate compiles by target kind (compileGate):
for a human-seat target, the packet goes to the gated step's own owner and is
parked blocked_on that human seat in the same transaction (summary and
evidence_ref required), and the instance waits until the park is resolved;
for a declared role, an ordinary packet goes to that handler role's seat
(workflow-projector.ts:1659–1778).
A preferred_targets entry may end in @{rig}, for example watcher@{rig}.
When the instance is bound to a rig (--rig, or the spec's target.rig
default), the trailing {rig} becomes that rig, so one reusable spec addresses
that rig's member, including a builtin:terminal member. Only a trailing
@{rig} is the form: braces in the member part or a partial rig name
(ops-{rig}@rig-a, ops-watcher@prefix-{rig}) stay literal. Literal
member@rig entries are unchanged. With no bound rig the entry stays literal
and is handled like any other destination, at the step that selects it: a
selected member@{rig} is refused as an unknown destination rig
(unknown_destination_rig), and rig workflow validate reports such an
exception target as unregistered. A {rig} in a role that is never selected,
or in a preferred target that is not selected, does not stop an instantiate.
The cached spec keeps the literal {rig}; the expansion is computed from the
instance's stored bound_rig each time its spec is read, so later hand-offs,
resume and exception routing use the same target
(workflow-rig-placeholder.ts, WorkflowSpecCache.getForInstance). A
terminal member cannot act on a nudge, so it must poll its own queue for the
packet.
Instantiation also emits advisories for missing members in otherwise registered target rigs. That member check is advisory and does not imply all preferred targets received a live-readiness test.
Projection, waiting, and concurrency
WorkflowProjector.project checks active/waiting status, frontier membership,
packet-to-step identity, allowed exits, and applicable evidence requirements.
For a successful advance, queue closure, successor creation, trail entries,
frontier/version updates, durable events, and wake intent staging share the
transactional notification envelope. Terminal delivery happens after the
commit, and the runtime awaits it, so project, route, resume and exception
responses return after the delivery attempt; a terminal close without its
successor's staged intent rolls back. A committed packet or staged wake is
not evidence that its recipient consumed the message.
Each exit closes the packet through the queue's own closure path
(updateWithinTransaction, hot-potato rules intact): handoff records
handed-off with handed_off_to the next owner; waiting records blocked
with blocked_on the supplied blocker, or external-gate when none is given;
done records done with no-follow-on; and failed records done with
denied and the result note as target (workflow-projector.ts:1571–1631).
A dependency-graph handoff that fans out to more than one successor, or to
none, closes the packet done with no-follow-on.
Both executors absorb an identical waiting replay by matching the stored
closure intent; changed wait data is a new decision. A consumed terminal
packet is no longer on the frontier and is refused. On the serial path a
routed close moves the frontier to the new packet and leaves the instance
active, or waiting when the target step is gated; an unrouted waiting
keeps the closed packet on the frontier so the keepalive can find its owner;
failed sets failed, and done with nothing left sets completed. A
dependency graph is waiting while every frontier packet is blocked, and
failed when the frontier empties with an unresolved occurrence.
max_hops constrains routing relative to the current drive's baseline: a
route that would exceed it becomes a failed exit with a max_hops_exceeded
note and evidence, the packet closes and no successor is created
(workflow-projector.ts:419–443); resume establishes a new bounded drive
rather than erasing history.
Presence of depends_on selects projectDependencyGraph. It uses durable
packet bindings to advance eligible successors and record per-occurrence
failures without treating an unrelated live branch as completed. A step's
completion counts only while it is later, in trail append order, than its
prerequisites' latest completions, so a prerequisite re-run through a routed
exit sends its dependents round again (workflow-projector.ts:1004–1019).
Inspection reports unknown bindings rather than inventing a step from trail order.
The injected createWorkflowFrontierPredicate
lets the queue refuse ordinary terminal closure of live workflow packets.
Workflow mutations use the explicit workflow path; the queue does not need
to import the workflow domain to evaluate that predicate.
Recovery operations
WorkflowRuntime distinguishes these operations:
| Operation | Meaning |
|---|---|
route |
Replace a current packet's owner while retaining the step. This is not step advancement. Multiple frontier packets require an explicit packet selection. |
resume |
Redrive failed work to a newly resolved owner, recording the optional --decision text. A serial instance must be failed (instance_not_failed). Resume re-resolves the owner rather than copying it, re-bases hops_baseline, increments resume_count, and closes that occurrence's open exception items. Dependency failures use occurrence identity; multiple unresolved failures require selection. Identical occurrence/decision replay returns the recorded redrive, while changed decision bytes conflict. |
abort |
Explicitly stop unfinished work with an actor and reason: cancels every frontier packet, records a failed trail row per packet, sets aborted, and emits workflow.failed with reason aborted: <reason>. Refused only for completed or aborted instances. |
continue |
Read-only inspection of instance, trail, guidance, reconciliation, frontier, failures, unknowns, and boundary obligations. It does not mechanically advance a step. |
Deadlines and exceptions
evaluateStepDeadline
derives overdue state from frontier queue rows. Claimed work uses its closure
deadline, otherwise its claim time plus 4 hours (creation time if the
claim time is absent). Pending work uses creation time plus 4 hours.
Blocked waits are not overdue under this evaluator. This is a diagnostic,
not a substitute for packet existence or binding checks.
Instantiate and every routed projection arm a workflow-keepalive watchdog
job in the same transaction (one per live packet for dependency graphs, one
per instance for serial specs), evaluated every 15 minutes and quiet until the
step is overdue (workflow-keepalive-arming.ts:38). At each daemon start the
boot sweep re-arms keepalives for live instances, re-nudges pending frontier
packets that were never nudged, and surfaces overdue instances
(workflow-boot-sweep.ts). Neither path advances a step.
There are three exception classes: unmapped_failed, stuck_overdue and
human_gate_trip (workflow-exception.ts:34–38). An unrouted failed
exit creates one urgent workflow-exception queue item in the same
transaction, with evidence rig workflow trace <instance> and a body naming
the rig workflow resume command; a human gate's own parked packet carries the
human_gate_trip identity, so no second item is created.
resolveExceptionRoute
selects per-class configuration, then workflow default, host default, and
finally orchestrator-first behavior. An unresolved orchestrator route falls
back to registered-human selection. A human gate is intrinsically human
routed. Agent-routed exceptions use the ordinary workflow tier; human-routed
ones use human-gate.
workflow-human-destination.ts
handles human selection: the registered human named by
workspace.operator_seat_name, or the only registered human when it is unset;
any other case fails with workflow_human_destination_unavailable (409). The
host default dial is the setting workflow.exception_routing
(orchestrator or human_only), read per exception. A valid agent route need
not have a human fallback configured. If fallback is needed, missing/ambiguous selection is an explicit
error, not an invented destination. Failure admission is transactional;
overdue detection is handled by boot/keepalive paths. Runtime reconciliation
closes recovered overdue occurrences against their own packets, preserving
unresolved siblings and unknown provenance.
Project lifecycle graphs
compileLifecycle delegates to
compileProjectLifecycle.
instantiateLifecycle requires an operation key and compiled input digest.
An exact replay retrieves the recorded instance/entry packet; changed input
under the same key conflicts. Compilation and an eligible result do not by
themselves launch work.
inspectGraph, reviseGraph, and recoverGraphOperation in
workflow-reconciliation.ts
separate inspection from explicit graph revision and operation recovery.
Inspection reports current, source-only, compatible, incompatible,
unavailable or unbound. Only a live, manifest-bound instance whose specs
are explicit dependency graphs without next_hop.on can be revised; a
revision may add steps and change unstarted ones, but may not remove steps,
touch a step with completed, live or failed work, drop a required obligation
or its order, change workflow-wide fields other than exception_routing, or
add an unstarted step with no outstanding prerequisite. A changed
exception_routing applies to future occurrences only; existing obligations
keep their owners. Applying needs the inspected version and digest, an
operation key, an actor and a reason; the receipt is kept in the instance's
revisionHistory, and replaying the same key with a different decision is
refused with lifecycle_revision_conflict (workflow-reconciliation.ts:12,
:113–223).
guidance reads context and current packet information without interpreting
an agent's substantive judgment.
For lifecycle steps that require a receipt, a done/handoff projection
requires an evidence_ref. Typed acceptance gates additionally compare the
submitted candidate, allowed verdict, and evidence reference against the
step declaration. These are mechanical identity/presence checks; the
projector does not read the referenced prose to decide whether the work is
good or ready to publish.
CLI and HTTP surface
The CLI inventory lists every registered
rig workflow subcommand and option. The implementation is
packages/cli/src/commands/workflow.ts.
The workflow routes
are mounted under /api/workflow:
| Method | Suffixes |
|---|---|
POST |
/validate, /compile, /instantiate, /instantiate-lifecycle, /project |
GET |
/list, /specs, /operations/:key, /:instance_id, /:instance_id/trace, /:instance_id/guidance, /:instance_id/revision |
POST |
/:instance_id/route, /:instance_id/resume, /:instance_id/abort, /:instance_id/continue, /:instance_id/revision |
GET |
/sse, /watch (same live event stream handler) |
The stream carries the 8 workflow.* events (instantiated, step_closed,
next_qitem_projected, completed, failed, resumed,
routing_table_changed, revised); it is not a general queue stream or a
stored-history replay. Route error mapping distinguishes missing
records, authoring errors, state/version conflicts, and internal failures.
See the source mapping before assigning a meaning to a nonzero CLI result.
For authoring examples and project-level boundaries, use product journey SDLC and release boundaries.