Mutation pipeline
The single canonical path every domain mutation travels — from caller to AssetEvent.
The mutation pipeline is the most important diagram in the platform. Every change to domain state — by a human, an agent, a webhook, or a worker — passes through it.
The pipeline
Stage by stage
The stages are normative. Their reusable implementation lives in packages/platform-host; a host's durable
workflow adapter calls the package's worker entry point.
Applications should consume @fabricorg/platform-host
instead of copying these stages into a vertical runtime. The package exposes submitAction() for trusted,
authenticated ingress and executeInvocation() as the durable worker entry point. Hosts provide database,
authorization, entitlement, and workflow-dispatch ports; the package owns stage ordering and terminal-status
classification. Omitting the dispatcher executes inline and is intended only for tests and local development.
1. Resolve and gate
invokeAction looks up the ActionDefinition in the registry. Two pre-flight checks run:
- Module entitlement — is
actionDef.namespaceenabled for this tenant? - Permission check — for human callers (
natural_person), does theMemberrow carryrequiredPermissions/requiredRoles? Skipped foragent,system,external_system.
2. Persist the invocation
A new ActionInvocation row is created with status: "pending" before any work happens. This means the audit trail records every attempted action — even ones that fail later.
3. Validate parameters
The action schema validates the durable parameters before any HITL or ordinary policy evaluator runs.
Invalid input ends as validation_failed and cannot be presented to an approver as executable work.
4. Evaluate agent HITL routing
When the actor is an agent and the host has a hitlEvaluator, the host calls the vertical-owned
evaluator before ordinary policies. The evaluator owns domain rules and returns a route, risk tier,
and reason; the host owns persistence and lifecycle behavior:
auto-executecontinues immediately.needs-approvalandescalatepersist their audit evidence and park the invocation aswaiting_for_approval. This status is not worker-claimable.rejectedterminally fails before policy evaluation or mutation code.
The optional seam is backward-compatible: absent an evaluator, agent execution is unchanged. Approval
workflows must call resumeApprovedInvocation() with the decision and approver identity. The host
authorizes the approver, then atomically persists the decision while transitioning the parked row to a
leased running state (approved) or failed (rejected). A worker cannot race the approval transition,
and a crash after approval uses the existing lease-recovery checkpoints without evaluating HITL again.
5. Revalidate execution authorization
The Host calls authorizeExecution with the canonical invocation, original actor, schema-parsed durable
parameters, opaque authorizationBindingId, and an execution reason of initial, approval_resume, or
recovery. Applications use this boundary to revalidate current registration status, grant expiry,
action authority, and resource scope.
Submission authorization is not proof of continuing authority. Revocation between admission and
execution fails before policies and mutation code. If authorizeExecution is not configured, the Host
reuses its submission authorize callback at this boundary.
6. Evaluate policies
For each policy ID declared on the action, the platform's policy engine evaluates it (evaluatePolicyDefinitions). Each policy returns pass, warn, or block. A single block halts the invocation with status: blocked_by_policy. See policy enforcement.
7. Resolve action kind
actionDef.kind is "atomic" (default) or "saga".
- Atomic → run
actionDef.handler(ctx, params)inside one DB transaction. - Saga → dispatch to a registered
SagaImplementationthat orchestrates child action invocations and may sleep/wait.
8. State-machine validation (atomic only)
If actionDef.stateMachine binds the action to an entity, the engine looks up the entity's current state, computes the target state, and checks that (from, to, actionId) is a registered transition (packages/platform/state-machines/engine.ts). Invalid transitions throw before the handler runs.
9. Handler
The handler is the single place vertical business logic lives. It:
- Parses parameters with its own schema (Zod is canonical).
- Reads/writes domain state on
ctx.db. - Returns
{ success, data?, error? }.
If the handler's parse throws a Zod-shaped error, the worker classifies it as validation_failed instead of failed, preserving the distinction between "input was wrong" and "execution blew up."
10. Append events
A mutating handler must emit at least one AssetEvent (registerAction enforces this — an action with mutatesDomain: true and an empty emitsEvents list throws on registration). Intent/outbox events append after the handler succeeds and before adapters run. Completion-style actions declare eventPhase: "after_adapters"; their events append only after every adapter succeeds, so an event named "executed" cannot precede the external side effect it attests to.
With PostgresPlatformHostStore and an application transaction provider, before_adapters domain writes
and declared events commit or roll back together. For after_adapters, the completion event and
invocation completion commit together. If that finalization fails, the invocation remains recoverable
and succeeded adapter checkpoints are not repeated.
11. Execute adapter steps
For each AdapterStep declared on the action, the runtime resolves the adapter from the registry and runs it through executeWithAdapterRetry:
- Idempotency is opt-in. Non-idempotent operations are forced to
maxAttempts: 1. - Idempotent operations get exponential backoff with caps (defaults: 3 attempts, 100 ms initial, ×2, capped at 1 s — see
packages/platform/adapters/index.ts). - A circuit breaker can be configured per adapter in
monitororenforcemode. - Recovery skips adapter checkpoints already marked
succeeded; ambiguous provider outcomes require an explicit governed reconciliation path instead of a blind resend.
12. Complete
The invocation row is updated to status: completed (or failed). Telemetry is emitted.
Failure classifications
The pipeline distinguishes failure kinds in ActionStatus:
| Status | Meaning |
|---|---|
pending | Row created, workflow not yet started. |
running | Workflow active. |
blocked_by_policy | A policy returned block. |
waiting_for_approval | HITL evidence is durable; workers cannot claim it until an authorized decision. |
validation_failed | Handler input failed schema validation. Distinct from failed. |
failed | Execution error (DB, adapter, unhandled throw). |
completed | Success. |
See also
- Stage details — deeper drill into each stage.
- Policy checkpoints — where in the pipeline policies fire.
- Events as evidence — what the pipeline produces.