Workflows
A Workflow is a reusable, versioned automation that an operator authors once and runs many times. It is a directed graph of typed steps — LLM call sites, system reads, inline write actions, service operations, human approval gates, and control-flow constructs (branch / loop / dynamic fan-out) — wired together by pins. When you run a workflow, Genesis compiles the graph into a mission whose tasks execute on the existing mission engine, with every gate paused behind a human decision and the originating workflowId@version + checksum pinned onto the run as compliance provenance.
Workflows are per-tenant authoring artifacts: every tool and endpoint is gated by the tenant-admin policy, and tenancy is implicit via the per-tenant database (no TenantId column — ADR-008), exactly like missions and tasks.
This page covers both surfaces:
- Operator / product — what a workflow is, its lifecycle, the MCP tool surface, the REST API, and the dashboard pages.
- Architecture — the data model, the graph IR and pin model, the step catalog, materialize → publish → invoke → run internals, system vs tenant workflows, triggers, compliance governance, the mission-run acceptance layer (runs → changements), and how workflows relate to missions and agents.
1. Operator guide
1.1 What a workflow is
| Concept | Meaning |
|---|---|
| Workflow | A stable logical identity (workflowId, e.g. wf3f9ac21b04) that owns an ordered series of immutable versions. |
| Version | One monotonic revision (1, 2, 3, …) of a workflow. A version is a graph payload plus a lifecycle status. |
| Graph | A JSON object { irVersion, nodes, edges, params } describing the steps and how their pins connect. |
| Step (node) | One unit of work: an LLM call site, a read, a write action, a service operation, a gate, or a control-flow construct. |
| Pin | A typed connection point on a step. Exec pins (in/out) order execution; data pins (Json/String/Number/Bool/Document/Artifact/Context) carry typed values. |
| Gate | A human approval step. Every write-capable step must sit behind a gate (enforced at publish). |
| Run | A mission launched from a published version. There is no separate "run" table — a run is a mission stamped with workflow provenance. |
| Tunnel | A workflow's own input/output pin signature, used when it is embedded as a step inside another workflow (FlowRef composition). |
1.2 The lifecycle
update_draft / materialize / waive_control / validate_draft
┌───────────────────────────┐
▼ │
create_draft ───► DRAFT ──── publish ───► PUBLISHED ──── archive ───► ARCHIVED
│ │ (validate + freeze) │
│ │ │
│ delete_draft invoke ───► RUN (a mission)
│ (permanent) │
└─ clone_from_mission / clone_from_system ────┘ (always lands a new DRAFT)
The only legal status transitions are Draft → Published and Published → Archived:
- Draft — the mutable working version; the only state that accepts graph edits. A new workflow's first draft is version 1; versioning an existing workflow adds
max(version)+1. - Update — replace the draft's graph/tunnels payload as many times as needed.
- Materialize (optional, governed workflows) — inject the required compliance gate/review steps from the active control catalog before publish, so the operator reviews them on the canvas.
- Publish — validate, then freeze. Validation reports every problem at once (catalog/site checks, pin compatibility, required pins, cycles, gate dominance, compliance floor). On success the version becomes immutable, gets a SHA-256 checksum, stamps
PublishedAt, and becomes invocable. At any point before that,workflow_validate_draftruns the identical validation as a dry run — the same aggregated problem list, no freeze, no status change. - Invoke — run a published version as a new mission. Multiple published versions of the same workflow can coexist; you choose which one to run.
- Runs — observe the launched missions per-workflow, per-version, and per-node.
- Archive — retire a published version (terminal; no longer invocable, kept for provenance). Delete is allowed only on drafts; published versions are immutable and must be archived instead.
1.3 The MCP tool surface
All tools live in WorkflowMcpTools (deferred names workflow_*), are gated by TenantAdminPolicy, and call the same services the REST controllers use through an in-process scope (Core is the service — no HTTP hop). Every tool returns a { success, data } (or { success:false, error, code }) envelope.
Authoring
| Tool | Purpose | Key inputs | Returns |
|---|---|---|---|
workflow_create_draft | Create a new draft version from a graph payload. | graphJson; optional workflowId (omit = new identity, version 1; supply = next version), tunnelsJson | { id, workflowId, version, status } |
workflow_update_draft | Replace a draft's graph/tunnels. Drafts only. | id (GUID), graphJson, optional tunnelsJson | { id, workflowId, version, status }. Errors: NOT_FOUND, CONFLICT (not a draft) |
workflow_materialize | Inject required compliance controls (human-gate + regulatory-review nodes) into a draft from the active control catalog, keyed off the development stages the nodes are tagged with. Runs before publish; deterministic + idempotent. | id | { id, workflowId, version, status } |
workflow_waive_control | Record an explicit, audited waiver authorising publish despite a missing required control. | id, controlId (e.g. DORA-P1-PROT-004), reason | { id, workflowId, version, status } |
workflow_validate_draft | Dry-run the full publish validation on a draft without freezing it — the same composite validator publish runs, every problem reported at once; the draft keeps its status. | id | The aggregated problem list (empty when the draft would publish clean) |
workflow_publish | Validate + freeze a draft. | id | { id, workflowId, version, status, checksum, publishedAt }. Error: VALIDATION_ERROR (lists every issue) |
workflow_rename | Set the workflow's display name (shared by all versions; outside the checksum, so renaming a published workflow is allowed). | workflowId, name | { workflowId, name, updatedVersions } |
workflow_archive | Archive a published version (terminal). Guarded: refused while a live published FlowRef consumer still references the workflow — check workflow_referenced_by first. | id | { id, workflowId, version, status }. Error: CONFLICT (not Published) |
workflow_delete_draft | Permanently delete a draft version. | id | { id }. Error: CONFLICT (not Draft). Marked destructive. |
Discovery / reads
| Tool | Purpose | Returns |
|---|---|---|
workflow_catalog | List the step-type palette — every node type you can place, with its typed input/output pins. Use before authoring so you wire compatible pins. category is the palette taxonomy family (e.g. Missions/Research/Validation); tags is a reserved sub-family axis — empty on every node today; lifecycle is Standard or Legacy (placing a Legacy node in a new draft is publish-blocked). | { catalog: [{ nodeTypeId, kind, siteId, category, tags, lifecycle, inputs:[{name,kind,required,defaultValue,variadic}], outputs:[…] }] } |
workflow_list | List all versions of one workflow, newest first. | { versions: [{ id, workflowId, version, status, checksum, createdBy, createdAt, publishedAt }] } |
workflow_list_all | List the latest version of every workflow in the tenant. | { workflows: [{ id, workflowId, name, version, status, … }] } |
workflow_get | One version with its full graph payload. version=0 (or omit) = latest published. | { id, workflowId, version, status, checksum, graphJson, tunnelsJson, … } |
workflow_runs | Missions launched from a workflow (its runs), newest first; optional version filter. | { runs: [{ missionId, title, status, version, createdAt }] } |
workflow_run_nodes | Per-step status of one run: maps the mission's tasks back to authoring node ids (the run-debugger view). | { nodes: [{ nodeId, taskDefId, status, resultSummary }] } |
Composition
| Tool | Purpose | Returns |
|---|---|---|
workflow_tile_catalog | List the published workflows available as embeddable FlowRef tiles (including the seeded sys- compositions), each with the tunnel signature you wire against. Use it before placing a FlowRef step. | The tile list (workflow identity, version, name, tunnel pins) |
workflow_rebind_flowref | Repoint a draft's FlowRef step at a different target workflow/version (move a consumer off an old sub-workflow). Rebinding invalidates the gates' prior ratification — they must be re-approved. | The updated draft envelope |
workflow_referenced_by | List the published workflows whose FlowRef steps reference a given workflow — the archive guard: check it before workflow_archive. | The consumer list (empty = safe to archive) |
Execution
workflow_invoke — run a published version.
- Inputs:
workflowId,version(must be ≥ 1 — there is noversion=0shorthand here; running "whatever is latest" must be an explicit choice), optionalobjective,model,provider,paramsJson(a JSON object of run-parameter values), andprojectId(optional target project the run's coder writes into — GUID or slug; unknown → 404, non-Active → 409, omitted → tenant default project, recorded in provenance asprojectScope). - Behavior: compiles the graph into a mission whose tasks carry the step wiring; every gate step is paused behind a pending approval decision (resolve with
decision_respond:approvereleases the gate once prior steps complete,skipcancels it,failfails it).model+providerare required unless tenant defaults resolve them — there is no silent model fallback. - Returns:
{ missionId, workflowId, version, checksum, stepCount, gateCount, controlCount }. - Follow-ups:
mission_getto watch the run;decision_listto find pending gate approvals;workflow_run_nodesfor per-step status.
Cloning (save a run, or fork a template)
| Tool | Purpose | Returns |
|---|---|---|
workflow_clone_from_mission | "Save this run as a workflow." Clones a mission into a new draft (never publishes). Workflow-born missions are re-drafted verbatim from their source; other missions are reconstructed from their task rows, adding a trigger and (only when needed) a single gate so the result is publishable. | { id, workflowId, version, status, source, notes, suggestedInvoke }. source = workflow-born | organic; notes lists every repair |
workflow_clone_from_system | Clone a platform system-workflow template into a new editable draft owned by your tenant. | { id, workflowId, version, status } |
System-workflow templates
| Tool | Purpose | Returns |
|---|---|---|
workflow_system_list | List the platform's read-only system-workflow templates (the built-in orchestration flows: mission lifecycle, research, onboarding, support, review, …). | { workflows: [{ workflowKey, name, description, category, checksum, isSystemSeed }] } |
workflow_system_get | One template with its full graph payload, so you can inspect the flow before cloning. | { workflowKey, name, description, category, checksum, graphJson, tunnelsJson } |
Node instructions
Every workflow node can carry node instructions — extra text layered onto the node's prompt at dispatch. Instructions live in scoped slots with precedence run > project > tenant > global (the most specific slot wins). Three tools manage them, gated by the same tenant-admin policy as the rest of the surface:
| Tool | Purpose |
|---|---|
workflow_node_instructions_get | Read a node's instruction slots across scopes. |
workflow_node_instructions_set | Write an instruction at one scope. Only MANUAL-source slots are writable — a slot owned by the PromptEngineer path returns CONFLICT. |
workflow_node_instructions_clear | Remove an instruction slot. Same MANUAL-only rule. |
Today every live slot is set by hand; the PromptEngineer proposal path (ARDS proposing prompt improvements itself) is not live in v1 — the CONFLICT guard reserves its slots.
1.4 REST API
Base route api/v1/workflows (tenant-admin). Mirrors the MCP surface for the dashboard composer:
GET /workflows— one row per workflow identity (latest version).GET /workflows/catalog— the step-type palette.GET /workflows/{workflowId}/versions— all versions, newest first.GET /workflows/{workflowId}/{version}— one version with the full payload.POST /workflows— create a draft (body:graphJson, optionalworkflowId/tunnelsJson/name).PUT /workflows/{id}— replace a draft's payload.POST /workflows/{workflowId}/rename— set the display name on all versions.POST /workflows/{id}/materialize·POST /workflows/{id}/waive— compliance overlay + waiver.POST /workflows/{id}/publish— validate + freeze. Note: completed validation is always HTTP 200 with a{ success, workflow, problems[] }envelope (success:false+ a list of structured{ nodeId, pin, message, raw }problems on failure) — the dashboard maps any non-2xx to "request failed", so a validation failure must stay a 200 envelope. 404/409 remain for missing/non-draft rows.POST /workflows/{id}/archive·DELETE /workflows/{id}— archive (published) / delete (draft).POST /workflows/{workflowId}/{version}/invoke— run.POST /workflows/clone-from-mission— clone a mission to a draft.GET/POST /workflows/system[...]— list/get/clone system templates.GET /workflows/{workflowId}/runsand/runs/{missionId}/nodesandGET /workflows/runs(fleet) — run observability (§2.10).
Trigger CRUD lives under api/v1/workflows/{workflowId}/triggers (§2.9); the run-centric read surface also has api/v1/workflow-runs/{missionId}/graph-status and GET /workflow-runs (in-progress runs).
1.5 Dashboard pages
The Blazor composer (Genesis.UI) provides:
| Route | Page | What it does |
|---|---|---|
/workflows | WorkflowsDashboard | List all workflows; create, open, clone. |
/workflows/{WorkflowId}/{Version} | WorkflowEditorPage | The graph editor / canvas: place steps from the catalog, wire pins, materialize, waive, publish, rename, manage triggers (WorkflowTriggersDialog). |
/workflows/system/{Key} | SystemWorkflowPreviewPage | Preview a system template before cloning. |
/workflows/runs/{MissionId} | WorkflowRunViewerPage | Live per-node run view (the run debugger), with WorkflowGateReviewPanel for resolving gates. |
/workflows/active | WorkflowActiveRunsPage | Cross-workflow list of in-progress runs. |
2. Architecture
2.1 Data model
Workflows persist in three places. Critically, runs are not a workflow table — a run is a Mission + its TaskDefinition rows, stamped with workflow provenance in Mission.Metadata["workflow"].
workflow_records (tenant DB — WorkflowRecord): one row per (workflowId, version).
| Column | Type | Notes |
|---|---|---|
id | guid (PK) | |
workflow_id | varchar(64) | stable logical identity (wf + 10 hex); shared by all versions |
version | int | monotonic, 1-based; unique index (workflow_id, version) |
name | varchar(200) null | display name, logical per workflow id, outside the checksum |
status | varchar(50) | Draft / Published / Archived (enum→string); indexed |
graph_json | jsonb | the graph IR payload; schema is versioned inside (irVersion) |
tunnels_json | jsonb null | the workflow's pin signature for FlowRef composition |
checksum | varchar(64) | lowercase SHA-256 hex of graph_json, computed at publish; empty while Draft |
created_by, created_at, published_at | created_at indexed | |
compliance_catalog_version | int null | the control-catalog version pinned at publish (provenance) |
compliance_waivers | jsonb null | logged waivers [{controlId, reason, grantedBy, grantedAt}] |
workflow_triggers (tenant DB — WorkflowTrigger): automatic trigger bindings, not graph-embedded (so they survive re-versioning). Columns: id, workflow_id, version_policy (LatestPublished/Pinned), pinned_version, trigger_type (Schedule/Webhook/Event), config_json (jsonb), webhook_secret (write-only), selected_model, selected_provider (both required — invoke has no fallback), objective, params_json, target_project_id, is_active, last_run_at, next_run_at, audit columns. Indexed (workflow_id, is_active) and (trigger_type, is_active, next_run_at).
system_workflows (control-plane DB — SystemWorkflowRecord): platform templates surfaced read-only to every tenant. Columns: id, workflow_key (unique), name, description, category, scope (Tenant/Platform), is_system_seed, status, graph_json, tunnels_json, checksum, timestamps. Reconciled from the code catalog on startup; tenants may clone but not edit them.
The compliance control catalog (workflow_control_catalog rows, control-plane + tenant) backs materialize/floor checks; it is documented under Compliance.
2.2 The graph IR and node/pin model
WorkflowRecord.GraphJson parses (via WorkflowIrSerializer) into a WorkflowIr record:
WorkflowIr { int IrVersion, IrNode[] Nodes, WorkflowEdge[] Edges, WorkflowParam[]? Params }
WorkflowEdge { PinRef From, PinRef To } PinRef { string NodeId, string Pin }
Node kinds (IrNodeKind) — 16 kinds:
| Kind | Authorable? | Role |
|---|---|---|
Event | yes | The trigger. At most one per graph; no input pins; its exec successors are the entry steps. Lowers to nothing. |
CallSite | yes | A latent LLM call site from the curated catalog. Write-classified (it dispatches coders). |
Read | yes | A pure, side-effect-free system read. Never write-classified, never gated. |
Action | yes | An inline-executed workspace:* write (W4). Write-classified; runs inline, never dispatches a coder. |
ServiceAdapter | yes | An inline-executed C# service operation (W5). Write-classified; runs inline. |
Gate | yes | A human approval gate. Lowers onto the Paused / AttentionRequest / DecisionResolution trio. Carries named decisions[] (human-labelled arms), typed capture fields[] surfaced on the response data pin, and an optional timeout resolving fail or skip. |
Branch | yes | First-match routing over a selector value; ordered cases + one else. Lowered to per-case Control steps. |
Loop | yes | Retry-until-acceptance over a published body workflow (flowId@version). Unrolled to N iterations. |
MapFanOut | yes | Dynamic fan-out: at runtime reads a JSON array from an upstream result and creates one body task per element (count unknown at compile time). Config: sourcePin (the upstream array), itemVar, a body that is either a single catalog step (bodyTemplate) or a sub-workflow (flowRef), maxItems (1..1000), and sibling dependencies via itemDependsOnPath (items form a DAG, not just a flat batch). |
FlowRef | yes | A composite step referencing another workflow by flowId@version; inlined at compile time. |
ForEach | yes | Sequential fan-out: iterates a JSON array one element at a time, in order, with break support (a satisfied break condition skips the remaining items). Retires Loop for new authoring; existing Loop graphs keep running. |
Wait | yes | Parks the run on a timer (durationMinutes, 1..40320 — one minute to 28 days) or an event (one of the six live seams — §2.9); exactly one of the two modes per node. |
Llm | yes | The generic bounded single-turn LLM step: one call, no tools. Its config siteId must name a registered deny-all site (checked at publish). |
Agent | yes | The generic in-process multi-turn agent step with an explicit toolset + budget; the toolset must be ⊆ the site's effective policy. Bi-class: read- or write-classified by its toolset (any write tool ⇒ write-classified, gate-dominated). Available but new — no built-in flow uses it yet. |
Finally | yes | A cleanup region that runs exactly once, whether the guarded region succeeds, fails, or is cancelled. |
Control | no | Compiler-synthesized routing (branch cases, loop continue/exit, gate skip arm, decision arms, outcome arms, map materializer/join). Authoring it is rejected. |
A latent LLM site that fires exactly one terminal outcome (onSuccess/onFailure/onRefusal) is an async call site; its descriptor exposes outcome pins instead of a single out, and the runtime fires exactly one arm (OutcomeRole).
Pins (PinKind): Exec (sequencing only, never carries data, never widens), Context, Document, Artifact, Json (may carry a JSON-Schema refinement), String, Number, Bool. Wiring is legal when kinds match (plus scalar→Json widenings); when both sides of a Json edge declare a schema, structural subset compatibility is enforced at publish. A PinDescriptor carries Name, Schema (kind + optional schema), Required, an optional editor-only DefaultValue, and Variadic (a data pin that accepts >1 incoming edge — an N-way fan-in collected into an array at runtime).
Params (WorkflowParam { Name, JsonSchema?, Required, default? }): graph-level invocation parameters. Values passed to workflow_invoke(paramsJson) are resolved at compile time into the bound pins' literals — there is no runtime parameter plumbing. A param bound to a required pin must itself be required or carry a default (checked at publish), so a published workflow can never compile into a task missing a required input.
Vocabulary / wire-format note: the IR persists a node's track under a legacy field name, kept stable so published checksums stay byte-identical. In all prose, UI, and tooling this field is the step's Track (Governance / Research / Planning / Build); the compiler defaults it to Build.
2.3 The step catalog (palette)
WorkflowNodeCatalog.All is the curated, code-defined palette of 128 step types (pinned live census). Each entry is a WorkflowNodeDescriptor { NodeTypeId, Kind (LlmSite/ReadAdapter/ActionAdapter/ServiceAdapter), SiteId, Inputs[], Outputs[], Outcomes?, ConfigFields? }. The descriptor attaches typed pins alongside the existing call-site registry — it never modifies LlmCallSite in Genesis.Contracts; an unannotated site is simply not palette-eligible. Representative entries:
- LLM call sites:
Missions:Decomposition,Coder:Default(sync) andCoder:AsyncDefault(outcome arms),ReviewEngine:Default. Every LLM site also carries two optional ghost-evaluation pins (ghostEval,ghostEvalModel). The Coder sites exposeConfigFields(Task Type, Track, Coder Variant, pinned Preset id) that the Properties panel renders and the compiler/validator read. - Read adapters (pure, never gated):
read:mission_get,read:gitlab_get_file,read:intelligence_knowledge_query,read:loki_query_range,read:task-deps:resolve,read:boundary:validate,read:task-routing:route,workspace:read_file,workspace:list_files. - Action adapters (inline workspace writes, gate-dominated):
workspace:write_file,workspace:create. - Service adapters (inline C# operations, gate-dominated):
workspace:merge,service:decomposition:propose, theservice:research:*family (investigate-codebase/docs/web, synthesize, critique, llm), theservice:review-engine:*,service:ux-review:*,service:canary:*,service:improvement:*,service:support:*,service:onboarding:*,service:aspect-onboarding:*,service:validation-runs:*,service:batch-processing:*,service:satisfaction:score,service:git-push:default, and theservice:compliance:*evaluators (SCA, clause-map, ROI completeness, incident timeliness, and the CRA / PLD families).
Read adapters' result schemas come from the adapter DTO files (single source of truth). The Genesis.Missions library stays free of the Contracts dependency — the Genesis.Core publish validator cross-checks each SiteId against the live LlmSiteRegistry.
The census by taxonomy family (17 families): ComplianceChecks 24 · Validation 20 · Missions 11 · Scaffolding 9 · Onboarding 9 · Support 8 · Review 7 · Improvement 7 · Workspace 6 · Research 6 · Incidents 5 · Agents 4 · Git 3 · Knowledge 3 · ComplianceAssess 3 · Communication 2 · Legacy 1. By descriptor kind: ServiceAdapter 102, ReadAdapter 18, LlmSite 4, ActionAdapter 2 (workspace:write_file, workspace:create), plus the generic Llm and Agent entries (one each). Exactly one node is Legacy — service:research:llm, superseded by the generic llm node and publish-blocked in new drafts.
Catalog-wide conventions: every node rides the Exec spine (in/out) — the sole exception is Coder:AsyncDefault, the one multi-arm node, whose exec exits are its outcome arms; 122 of 128 emit a required result:Json output; the six brain nodes (Missions:Decomposition, Coder:Default, Coder:AsyncDefault, ReviewEngine:Default, llm, agent) are the only ones carrying the context:Context pin, and the four wired LLM sites among them (Missions:Decomposition, Coder:Default, Coder:AsyncDefault, ReviewEngine:Default) are the ones where the ghost-evaluation (ghostEval/ghostEvalModel) pin pair is live; the fan-out families follow a prepare → per-item → reduce triad (the per-item body node's first input is item:Json, required); and a few nodes emit typed handles alongside — or instead of — result: runId (service:compliance-assess:prepare), incidentId (service:incident:create), leaseId (service:run-target:lease), workspace (workspace:create/workspace:merge).
2.4 Authoring and the compliance overlay (materialize)
materialize (ComplianceOverlayService.MaterializeDraftAsync) runs before publish so the operator reviews the injected controls on the canvas. It:
- Parses the draft IR.
- Reads the floor — the active control catalog (
IWorkflowControlCatalog.GetActiveAsync) — plus additive-only governance augmentations (IOverlayGovernanceAugmenter; the default reads nothing, the seam allows a project to add controls, never remove a floor control — the ratchet design). - Runs the deterministic
ComplianceOverlayMaterializer, which injects the required human-gate and regulatory-review/service:compliance:*nodes keyed off the development stages the graph's nodes are tagged with (Design/Implementation/Integration/Verification/PreDeployment), plus the base always-required controls. Injected nodes carry overlay provenance ({ controlId, catalogVersion }). - Idempotent: if the canonical JSON is unchanged it returns the draft untouched (no checksum churn). Otherwise it persists the parse-compatible wire form draft→draft.
waive_control appends an audited waiver { controlId, reason, grantedBy, grantedAt } to the draft so the publish floor treats that control as satisfied — unless the control is non-waivable, in which case the presence of a waiver is itself a publish violation.
2.5 Publish and validation
IWorkflowRepository.PublishAsync runs a single injected IWorkflowPublishValidator — a CompositeWorkflowPublishValidator that runs every registered validator and aggregates all problems (no short-circuit), so structural, registry, and compliance-floor issues surface together. The three validators:
- Structural —
WorkflowIrValidator(pure; no DB/registry). It runs the same way the compiler will: FlowRef-expand →Validate(Authored)→ control-flow-expand →Validate(Final). It checks: well-formed ids (unique, no reserved__/#segments), at most one Event, per-kind pin models, edge kind/schema compatibility, single-edge-per-data-pin (relaxed for variadic), required-pin connectivity, literal/param binding rules, branch/loop/fan-out configuration, branch-arm and outcome-arm reconvergence isolation, acyclicity (with an explicit cycle path), and — the cornerstone — gate dominance. - Registry —
LlmSiteRegistryWorkflowPublishValidator(Genesis.Core): re-runs the structural pipeline on the FlowRef-expanded graph, cross-checks every call site'sSiteIdagainstLlmSiteRegistry.GetAll(), validates the draft's tunnel signature (names/kinds/binds), and validates any node-pinned LLM preset id is known and eligible. - Compliance floor —
ComplianceFloorPublishValidator(Genesis.Core, fail-closed). A graph is governed iff any node carries astagetag or an overlay-injected node. Ungoverned graphs publish unchanged. For a governed graph: every write-classified node must carry a stage; a required control is satisfied only by a node matching its control id and node kind; a missing control passes only with an explicit waiver (and only if the control is waivable). On success it pins the active catalog version onto the record.
Gate dominance. The wave executor runs a task only when all its dependencies have completed. So a write-classified step (
CallSite/Action/ServiceAdapter) is execution-blocked by a gate iff aGatenode is an ancestor in the union (exec + data) dependency graph. The validator enforces exactly that: every dependency path to a write must traverse a Gate.Readsteps are never write-classified and never gated. The one exemption is an overlay-injectedservice:compliance:*check — it is the protection mechanism (deliberately placed before the stage gate so a failed check cascade-cancels the gate→push), not an author write.
Publish failure is reported as VALIDATION_ERROR over MCP and as a 200 { success:false, problems:[…] } envelope over REST; the controller parses each validator message into a structured { nodeId, pin, message, raw } for canvas highlighting.
2.6 Invocation: compiling a graph into a mission
WorkflowInvocationService.InvokeAsync is compile-first so a graph that no longer compiles (e.g. a referenced sub-flow was unpublished) never leaves an orphaned mission:
- Load + integrity. Fetch
(workflowId, version); requirePublished; recompute the SHA-256 ofgraph_jsonand refuse if it does not match the storedchecksum. - Compile (pure). Parse the IR, build a
FlowResolver(resolves FlowRef sub-flows; only Published versions resolve), generate a fresh per-invocationseed, parseparamsJson, and runWorkflowCompiler.Compile. The compiler is pure and deterministic — no DB, no clock, no fresh GUIDs; same graph + same seed → byte-identical task set. It:- FlowRef-expands, validates (Authored), control-flow-expands (Branch/Loop/MapFanOut →
Controlnodes), validates (Final), then applies params into literals. - Assigns deterministic
TaskDefIds (DeterministicIdGenerator.TaskDefId(seed, nodeId)), computes Kahn topological waves (theExecutionOrder), and lowers each node to aCompiledTaskcarryingTaskType,Track,CoderVariant,DependsOn, status, allowed tools, acceptance criteria, aworkflowcarrier (the JSON describing node id/kind/site/inputBindings/outputSchema/control config, used at dispatch), and — for gates — aCompiledGate. - Lowering by kind: Gate →
gate/Paused;Control→workflow-control/Paused;Read→analyze/Pending;Action→workflow-action/Pending;ServiceAdapter→workflow-service/Pending;CallSite→code(or configured)/Pending. Defense-in-depth guard throws against a non-gate row carrying the reservedgate/workflow-control/workflow-servicetask types (which would self-approve or mis-route).
- FlowRef-expands, validates (Authored), control-flow-expands (Branch/Loop/MapFanOut →
- Resolve the optional target project (fail-loud before the mission is created; an omitted target is logged and recorded as
projectScope=tenant-default). - Derive model/provider for a one-click UI invoke from a coder node's pinned preset (resolved through the same
Coder:Defaultsite dispatch uses) — additive and blank-gated, so a caller-supplied model/provider is a no-op here. - Create the mission with
Metadata.skipDecomposition=trueand aworkflowprovenance blob (workflowId,version,irChecksum,invocationSeed,paramsJson,targetProjectId,projectScope). Model/provider are forwarded toCreateMissionAsync, which fails loud when both are blank. - Materialize the tasks in one save (the ProposalMaterializer pattern) with pre-set
TaskDefIds and per-task status. Gate and control rows getMaxRetries=0(a rejected gate must stay rejected; a fail-resolved loop-exit must stay failed — auto-retry would otherwise resurrect them as "approved"). - Queue one approval request per gate through the decision queue (
IDecisionQueueService), carrying the gate's decision arms / typed fields in the AttentionRequest metadata, and an optional deadline + timeout action. If anything fails mid-materialization the mission is compensated toFailed(a half-materialized run with an unblockable gate must never park silently).
The result is { missionId, workflowId, version, checksum, stepCount, gateCount, controlCount } (compiler-synthesized control rows are excluded from stepCount and surfaced separately).
2.7 Runtime execution of a run
A run executes on the existing mission engine (ArdsMissionOrchestrator) — workflows add no new runtime. Because the mission is created skipDecomposition=true, the orchestrator never auto-decomposes it; it dispatches the pre-materialized tasks by wave. For workflow-provenance missions, each orchestrator tick runs a deterministic control pass (IWorkflowControlService.EvaluateAsync) before the Ready-dispatch and Pending→Ready promotion loops:
- It reads each
workflow-controltask's resolved carrier and the in-memory task list and runs to a fixpoint (guard-capped), only ever transitioning statuses of already-materialized tasks — it never creates tasks (except the MapFanOut materializer, which adds body work tasks idempotently). The routers:RouteBranches,RouteLoopContinues,RouteLoopExits,RouteGateSkipArms,RouteGateDecisionArms,RouteOutcomeArms,RouteMapFanOut,RouteMapJoins, andCascadeSkip(dead-region cancellation, markedworkflow-cascadeto distinguish from an operator skip). - Conditions prefer an authored boolean RulesEngine expression when present, else fall back to a dotted-path-plus-expected scalar equality — so every pre-expression graph behaves byte-identically.
Step execution by kind:
- Gate — born
Pausedwith an open AttentionRequest. The operator resolves it viadecision_respond/decision_create:approvereleases dependents once prior steps complete (pre-approval is allowed);skipcancels the gated region;failfails it. N-arm gates route on the decision pin (never the display label); typed gate fields are captured onto the gate'sResultSummaryand exposed via aresponsedata pin. - Read / Action / ServiceAdapter — run inline in the dispatch poller (never dispatch a coder): the orchestrator resolves the matching
IWorkflowReadAdapterExecutor/IWorkflowActionExecutor/IWorkflowServiceAdapterExecutor, passes the carrier's resolvedinputBindings(literals, upstream output references, and dispatch-time name-pattern bindings viaWorkflowReadInputResolver), and writes the result back. Action/ServiceAdapter rows are guarded: a row reaching dispatch without compiler carrier provenance is failed rather than run. - CallSite — materialized into a coding task and dispatched to a coder as usual; the carrier's node-pinned preset drives provider routing, and an async call site must emit a parseable
{"outcome":…}final line (the result consumer fails+retries a result that declares none).
2.8 System vs tenant workflows
The platform ships 22 system-workflow templates that represent the hardcoded orchestration flows as composable graphs, defined in code (SystemWorkflowCatalog) and reconciled into the control-plane system_workflows table on startup. The category breakdown: Mission ×6 (sys-mission-lifecycle, sys-research-mission, sys-decompose-direct, sys-mission-decompose-grounded, sys-decompose-research, sys-task-dispatch), Onboarding ×4 (sys-first-run-onboarding, sys-aspect-onboarding, sys-intelligence-scaffolding, sys-project-onboarding), Ops ×3 (sys-batch-processing, sys-canary, sys-run-acceptance), Compliance ×2 (sys-compliance-assess, sys-incident-pipeline), Review ×2 (sys-review-engine, sys-ux-review), Validation ×2 (sys-llm-preset-validation, sys-validation-runs), Evaluation ×1 (sys-satisfaction-scoring), Improvement ×1 (sys-improvement-cycle), Support ×1 (sys-support-case-triage). These exercise the real constructs — e.g. sys-mission-lifecycle is an engine-driven MapFanOut (one coder per proposed task, count unknown until decomposition runs); sys-research-mission runs 3 parallel investigators (a diamond) into a bounded synthesize→critique revise loop (max 3 passes, breaking the moment the critic approves).
System rows are read-only to tenants (ISystemWorkflowStore): tenants can list/get and clone into a tenant-owned draft (workflow_clone_from_system → fresh identity, graph verbatim), then edit/publish/invoke freely. Scope="Platform" templates are hidden from tenant surfaces.
Two catalogs, two purposes. SystemWorkflowCatalog holds the 22 representational templates above — read-only clonables: cloning one gives you the graph as an editable tenant draft, not a wire into the platform's hardcoded flow. Separately, SysWorkflowInventory seeds 19 executable sys- compositions, published per-tenant as real workflow versions and surfaced as FlowRef tiles for embedding (e.g. sys-research-codebase, sys-decompose-propose); most are seed-only today — the legacy call sites they mirror have not yet been re-pointed onto them. The full template enumeration lives in the user docs: the templates page.
A related capability, decomposition-as-workflow (MaterializeDecompositionWorkflowAsync), materializes a system decompose template (sys-decompose-direct / sys-decompose-research) directly onto an existing mission and auto-resolves the plain leading gate through the live decision path — the flag-gated seam that replaces the inline phase invoker for opted-in missions.
2.9 Triggers (automatic invocation)
A WorkflowTrigger binds a published workflow (by stable workflowId + a version policy: LatestPublished or Pinned) to an automatic source. Every source ends at the same call — resolve the published version and InvokeAsync — differing only in how "fire now" is detected:
- Schedule —
WorkflowTriggerScheduler(aBackgroundService, ~1-minute loop with a startup delay) fans out over the tenant registry and fires rows whoseNextRunAtis due, then advancesNextRunAtvia the shared cron parser. Missed-window policy: fire once and recompute forward (never catch up N occurrences);NextRunAtis the idempotency guard. - Webhook — an inbound GitLab event verified against the per-trigger
webhook_secret(constant-time, never logged), filtered byeventType+ afilterJson. - Event —
WorkflowTriggerDispatcherlooks up active Event triggers for an in-process domain event (the six live seams:mission.created,conversation.completed,support-case.created,incident.created,batch.completed,mr.merged), evaluates each trigger'smatchJson(projectId / keyword filters) against the event context, and invokes matches. A bounded(trigger, eventId)set gives at-most-once delivery per process; a loop guard skips dispatch entirely when the context carriestriggerSpawned:true, so a trigger-spawned mission cannot re-firemission.created.
Triggers are validated fail-loud at create time (the workflow must have a published version; selectedModel + selectedProvider are mandatory because invoke has no fallback; a Schedule cron must parse; a Webhook needs a secret + eventType). Triggering never bypasses or auto-approves a gate — fail-closed discipline is preserved. Triggers are not graph-embedded, so they survive re-versioning.
2.10 Run observability
Because a run is a mission, observability is a projection over its tasks rather than a dedicated store. WorkflowRunsReader reads Mission.Metadata["workflow"] provenance and maps each TaskDefinition back to its authoring node id via the carrier (shared by MCP and REST so they can't drift). Beyond the raw workflow_run_nodes shape, the read services project each node to a NodeRunState — the eight TaskDefinitionStatus values plus a derived AwaitingGate (a Paused task that has an open, unresolved AttentionRequest). The surfaces:
GET /workflows/{workflowId}/runs/{missionId}/nodes— per-node projected states, filterable bystate/nodeType/gateLabel.GET /workflows/runs— fleet view: cross-workflow runs with a per-runNodeRunStaterollup, sourced from the F003-safeGetMissionsAsync, filterable by the sharedRunFiltergrammar (template/missionId/time-window/providerModel at run level; state/nodeType/gateLabel at node level).GET /workflow-runs/{missionId}/graph-statusandGET /workflow-runs— the run-centric FE surface (per-node status + in-progress runs). The graph shape itself is fetched through the existing version API using the returnedworkflowId+version— it is deliberately not duplicated here.
(Fan-out body tasks are node-grain-incomplete in the per-run read until the body carrier carries provenance fields; the fleet rollup counts all real task rows.)
2.11 Clone-from-mission
WorkflowCloneService.CloneFromMissionAsync turns a run back into a draft via two paths chosen by what the mission carries:
- Workflow-born — the mission pins
workflowId@versionprovenance and the sourceWorkflowRecordstill exists: itsGraphJsonis re-drafted verbatim underTargetWorkflowId ?? sourceWorkflowId(a checksum drift only adds a note). - Organic — no usable provenance (a hand-built/decomposed mission, or one whose source record was deleted):
MissionGraphReconstructorrebuilds a graph from the mission's task rows, adding a trigger and — only when needed — a single approval gate so the result is publishable, reporting every repair in the notes.
It never publishes (always lands a Draft the author reviews) and surfaces the mission's objective + model/provider as run hints only — no pin-level parameters are extracted.
2.12 Relationship to missions and agents
- Missions are the substrate. A published workflow compiles deterministically into a mission's
TaskDefinitionset; the run executes on the existing wave executor, decision queue, and orchestrator sweeps. Workflows add the authoring/versioning/compliance layer; missions add nothing back into the graph. - Gates reuse the decision system. A gate is not a new entity — it is an AttentionRequest row + task
Paused+ missionPendingOperatorDecision, resolved by the samedecision_*tools agents and operators already use. - Agents are the executors.
CallSitesteps dispatch coders/agents exactly as decomposed mission tasks do, honoring per-node LLM presets through the same provider routing; read/action/service steps run inline against the same services. A workflow is therefore a governed, reusable, gate-enforced way to orchestrate the same agent fleet that ad-hoc missions use — with provenance (workflowId@version+ checksum) pinned onto every run as a compliance artifact.
2.13 Mission runs and changements (the acceptance layer)
workflow_invoke creates a fresh mission (§2.6). A published version can also be run inside an existing mission as a discrete, acceptable unit — a MissionRun — via mission_run_start (flag-gated Workflows:MissionRuns:StartEnabled; enabled on current deployments). It reuses the same compile-first pipeline, but the run is born on its own git branch off the mission tip (strict-sequential: the run's base SHA must still equal the tip), and every materialized task is stamped with the run id/branch.
mission_run_accept closes the loop and is the unit of acceptance:
- Re-check the run's base is still the mission tip (else a
StaleRefused— re-run off the current tip). - Run the non-waivable physical-validation floor against the run branch. This is fail-closed: with no live execution backend to validate against, the accept is refused (
ValidationRefused), never passed unvalidated. - Merge the run branch onto the mission tip and record the result as a Changement — a reversible unit carrying an ordinal, its touched files, and its dependencies on prior changements (derived by file overlap). The accepted tip is sealed, so the mission tip stays always-green.
mission_changement_stack lists the stack in ordinal order. mission_revert_preview + mission_revert undo a changement together with the transitive closure of everything that depends on it, in reverse order, each as a merge-revert — fail-closed on conflict (a conflict aborts the cascade cleanly and names the failing member). mission_run_discard drops an un-accepted run (marks it rejected, deletes its branch) without touching the stack; a competition run resolves to exactly one winner (siblings rejected, branches deleted). This is how a workflow's output becomes reviewable, acceptable, and reversible on the mission it runs in.
mission_run_start takes an optional groupId. Omit it and runs are strict-sequential: a second start while a run is still open is refused (RunAlreadyOpen) — accept or discard the open run first. Pass the same groupId to N starts and the runs compete from the same base SHA: exactly one can be accepted; the siblings are rejected and their branches deleted (GroupAlreadyWon on a late accept). mission_run_get returns the per-node roll-up plus the acceptance anchors — the changement, the physical-validation seal (tip SHA + verdict), and isAnchored, true iff the run is Accepted and its changement applied and its seal passed.
The two status vocabularies, verbatim. mission_run_start returns one of 11 statuses: Started · FeatureDisabled · AgenticMissionUnsupported · MissionTerminal · MissionNotPlanned · ChildSpawnerDisallowed · RunAlreadyOpen · TipUnavailable · WorkflowNotFound · NotInvocable · MaterializationFailed. mission_run_accept returns one of 7: Accepted · StaleRefused · MergeConflict · GroupAlreadyWon · ValidationRefused · NotProposed · TipUnavailable.