FabricFabricPlatform
Platform referenceArchitecture

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.namespace enabled for this tenant?
  • Permission check — for human callers (natural_person), does the Member row carry requiredPermissions / requiredRoles? Skipped for agent, 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 SagaImplementation that 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 monitor or enforce mode.

9. Complete

The invocation row is updated to status: completed (or failed). Telemetry is emitted.

Failure classifications

The pipeline distinguishes failure kinds in ActionStatus:

StatusMeaning
pendingRow created, workflow not yet started.
runningWorkflow active.
blocked_by_policyA policy returned block.
waiting_for_approvalAction requires human approval before continuing.
validation_failedHandler input failed schema validation. Distinct from failed.
failedExecution error (DB, adapter, unhandled throw).
completedSuccess.

See also

On this page