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. Source is packages/api/modules/platform/lib/invoke-action.ts and the worker action workflow.
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. 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.
4. 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.
5. 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.
6. 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."
7. 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). The events are appended after the handler succeeds.
8. 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.
9. 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 | Action requires human approval before continuing. |
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.