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. 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.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. 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-execute continues immediately.
  • needs-approval and escalate persist their audit evidence and park the invocation as waiting_for_approval. This status is not worker-claimable.
  • rejected terminally 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 SagaImplementation that 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 monitor or enforce mode.
  • 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:

StatusMeaning
pendingRow created, workflow not yet started.
runningWorkflow active.
blocked_by_policyA policy returned block.
waiting_for_approvalHITL evidence is durable; workers cannot claim it until an authorized decision.
validation_failedHandler input failed schema validation. Distinct from failed.
failedExecution error (DB, adapter, unhandled throw).
completedSuccess.

See also

On this page