A workflow is a directed acyclic graph stored in workflow_versions.graph_json. The canvas the operator drags into shape is the exact JSON the executor walks. What the admin sees on screen fires the moment the trigger arrives.

Every step claim takes a row-level lock and skips whatever another worker already holds. Every outbound side effect carries an idempotency-key header derived from tenant, event, run, step, and action type. Every failure that exhausts retries lands in workflow_dead_letters with the operator’s inline-resolve action visible on the same page. The engine borrows Temporal’s token-passing shape and runs entirely inside Postgres. One durable primitive; one deploy topology.

Six node types, all first-class

trigger, action, condition, delay, parallel, join. Every one is a real dispatch branch in the executor.

  • trigger — exactly one per workflow, zero inbound edges. Five surfaces: lifecycle events on any entity (record created, updated, deleted, completed; relation added or removed; invoice status changed), scheduled sweeps, row-relative sweeps (“30 minutes before meeting.starts_at”), HMAC-SHA256 signed inbound webhooks, and manual invocation from the record page, from the agent’s invoke_workflow tool, or from a POST to /workflows/by-key/{workflow_key}/invoke.
  • condition — two outgoing handles, true and false. Only the matching branch enqueues on success.
  • delay — pauses the branch. The successor queues with a next-attempt time equal to now plus the duration; the claim window admits it once the wait elapses.
  • parallel — fan-out. Every outgoing edge enqueues concurrently.
  • join — fan-in. The runtime creates the join step only once every predecessor has a succeeded step, so a slow branch holds the join open while other branches downstream of the fast ones continue.

A partial unique index on (workflow_run_id, workflow_node_id) makes step creation idempotent under fan-in. Two predecessor branches racing to spawn the join step converge on one row.

Thirteen actions, one registry

workflow_action_registry.py holds every action class. Each one ships an execute path, a simulator, and a manifest. The picker in the canvas inspector hydrates from GET /workflows/actions/catalogue. A new action written on the API becomes a new option in the editor after a backend deploy, with no SPA change.

The thirteen: record.create, record.update, record.set_field, record.assign_owner, record.update_many, relation.add, relation.remove, flow.sub_workflow, agent.run_step, ai.enrich_record, mcp.tool_call, external_mcp.tool_call, and emit_webhook.

Two carry the Signals narrative. external_mcp.tool_call reaches through the connectors layer — Microsoft 365 inbox trawl, a partner API, whatever adapter the tenant has installed. agent.run_step drives one or more turns of the embedded agent inside the workflow run. The runtime mints a short-lived JWT with session_kind="workflow_service", a 15-minute lifetime, and no permanent identity row. The auth layer recognises the kind, skips user-membership lookup, enforces tenant match, and reads the role from the token. Audit attribution stays on the workflow run itself. The ledger reconciles cleanly at month-end because every agent turn a workflow spawned rolls up to the same agent_usage_events meter as a direct chat turn.

Durable execution, on the substrate you already have

The claim primitive is a row-level lock that skips already-claimed rows, at workflow_repository.py:491. Multiple API replicas compete for the next step; exactly one wins; the rest move on. Step inserts collapse on the unique (workflow_run_id, workflow_node_id) pair at workflow_repository.py:318, so a replayed step converges to the existing row. The event-log gate on (tenant_id, event_id) means a scheduler tick redelivered mid-flight converges to the existing run.

Every outbound webhook carries the idempotency-key header. A downstream receiver that respects it converges to one delivery no matter how many times the retry ladder walks it. Every failure that exhausts its retry budget lands in workflow_dead_letters with a reason, the payload, and the operator’s Resolve action attached. The engine is a live, monitored control. This is the same idempotency posture the substrate enforces at every other write path.

Signals as a workflow

The canonical template inbox_signals_via_external_mcp ships in migration 198: an external tool call reaches into an installed inbox connector, an agent.run_step classifies what matters, record.create lands a task on the right account. The pattern replicates for any inbound stream — the Signals page reads through the same table.

workflow_templates is a platform-tier library. New tenants seed every active template as a draft on provisioning; existing tenants apply individual templates via the platform admin endpoint. “Save as platform template” promotes a tenant’s workflow upstream; “Apply to tenant” copies one back down. The library is a twenty-five-workflow seed catalogue — one entry per canonical revenue motion, each one an editable draft the moment it lands.

Activities to invoices, in one workflow

invoice.from_records reads a tenant’s activities marked To Be Billed, groups by account, and posts to /api/v1/invoices/from-records. Each line carries source_entity='activities' and source_id={activity_id} so a compliance auditor traces the line back to the activity that produced it. When the invoice writes, post_billing_status flips every source activity’s billing_status_id to Billed inside the same transaction. The recompute triggers on invoices.total / subtotal / tax_amount fire atomically with the line-item write.

Reverse the flow: mark an invoice deleted and the shipped invoice_deleted_reopens_activity_billing workflow flips the activities back to To Be Billed. Two workflows, one substrate, one audit row per state change. A CRO reading the pipeline against the ledger sees the same numbers the CFO reads against the AR report.

The takeaway

The workflow engine is a DAG the operator can see, an action registry the agent can call, and a step claim the database refuses to double-execute. agent.run_step and external_mcp.tool_call sit as first-class action types alongside record.create. A new action written on the API lands in the picker after one backend deploy. The durable execution story is the substrate itself.