Skip to content

The transactional execution core

Every mutation follows one rule: committed PostgreSQL rows are the input and output of the workflow transition. A replica does not continue from a mutable process object retained from an earlier request. This is the ACID rail that carries every workload identically — native APL, imported BPMN, decision tables, agent steps and the Insight loop’s terminal facts.

sequenceDiagram
    actor Client
    participant API as Controller
    participant Command as Core command
    participant DB as PostgreSQL
    participant Outbox as Outbox dispatcher

    Client->>API: mutation + identity + optional idempotency key
    API->>Command: typed command
    Command->>DB: begin transaction
    Command->>DB: load and lock authoritative rows
    DB-->>Command: committed state
    Command->>Command: authorize and validate
    Command->>Command: advance canonical state
    Command->>DB: persist state, work, facts, history and outbox
    Command->>DB: commit
    Command-->>Client: deterministic result
    Outbox->>DB: lease committed events
    Outbox-->>Client: retryable lifecycle delivery
  1. Load. Fetch the definition version and mutable aggregate records needed by the command.
  2. Lock. Use row locks for single-winner transitions and optimistic versions for stale-write detection.
  3. Authorize and validate. Check the authenticated permission, task assignment, current status, lease ownership and command inputs.
  4. Advance. Execute the canonical process model — APL and BPMN compile to the same graph — and create successor tokens or durable work.
  5. Persist. Store variables, tasks, subscriptions, jobs, history, terminal Insight facts, idempotency result and outbox events in the same transaction.
  6. Commit. Only a successful commit makes the transition visible.

The execution core separates what runs inside the transaction from what must not:

Concern Where it runs
Decision tables (FIRST/UNIQUE/COLLECT, otherwise fallback) Inside the transaction — reproducible, audited as DECISION_TABLE_APPLIED
Scripts and embedded delegates Inside the transaction, for deterministic local logic
Agent nodes and engine-tasks Durable external tasks; a worker reports back through a command
Insight terminal facts One idempotent row per terminal node visit, written in the command transaction
Model and tool calls Never inside a workflow transaction

A probabilistic output is never trusted as an engine decision: agent results merge through the normal external-task completion command, and Insight proposals are discarded unless AplParser validates them and a human approves them.

  • Tokens. Each path of an instance is a row in process_tokens (activity, scope, parent, loop counters, state). Joins count arrived token ids, so a parallel branch that ends early never blocks its join, and loop counters survive restarts. Waiting work names the token it belongs to.
  • Jobs. Timers, on_timeout deadlines and sla_hours checks are durable rows acquired with SKIP LOCKED, so exactly one replica fires each one.
  • Retiring work. When a step leaves early — a timeout, a cancellation, a decision — its open task, external job and timers are retired in the same transaction, before the instance lock, so a late worker report is rejected instead of advancing the process twice.
  • Agent work that waits. An agent waiting for a person’s approval of a tool call, or for a child process it delegated to, holds no lease and no worker; the decision or the child’s end makes it acquirable again on the same attempt. Its token stays on the agent step meanwhile.
  • Live events. Every history record also writes an outbox row with a sequence number, in the same transaction. Delivery to webhooks leases those rows; the project event stream that Studio follows tails the committed rows in sequence order, holding a gap for a few seconds while an earlier transaction may still commit. Events carry identifiers only.
  • Incidents. A path that cannot continue (a failed step without on_error, an exhausted loop without on_exhausted, a message wait without a correlation value) stops with an incident row. The rest of the instance keeps running; an operator retry restarts that path in a new command. An agent write interrupted without an idempotency key stops the same way, until a person says whether it happened.
  • Failure before commit rolls back workflow state, new work, facts, history and outbox records together.
  • Failure after commit leaves durable progress. An idempotency record lets a duplicate API mutation replay its logical response.
  • Scripts and embedded delegates run inside the transaction, but an irreversible external side effect cannot be rolled back by PostgreSQL.
  • External tasks are the preferred boundary for remote effects; workers must make at-least-once effects idempotent.

The sole execution cache stores immutable parsed definitions keyed by deployment ID. A new process start queries PostgreSQL for the latest version; an existing instance reloads its pinned version. Evicting the cache changes latency, not semantics.

Mutable instances, tokens, joins, tasks, subscriptions, timers, jobs, variables and facts never live in runtime-wide maps. Command-local maps are allowed only while materializing and advancing one locked aggregate.

Controller-reachable mutations enter services marked with @AtomicRuntimeCommand. AtomicRuntimeCommandContractTest inventories that boundary, while PostgreSQL rollback and two-context tests prove the behavior under failures and concurrent commands.