Docs · Architecture

The machine under the canvas

The overview names the four parts; this page specifies how they behave. Every claim here mirrors shipped code, and where a number appears (a ceiling, a depth, a default) it is the number the engine enforces, not a rounded aspiration.

The shape

Four units, one contract

Everything meets at the FlowGraph. The canvas writes it, the contract types and validates it, the engine executes it, and the runtime wraps the engine in HTTP, payments, and scheduling. No unit reaches around the contract to talk to another.

01

Canvas

src/components/canvas

The visual editor, built on @xyflow/react. It edits a plain data structure, never code: dragging a node adds an entry to a list, wiring an edge adds a connection between typed ports, and port compatibility is checked at wire time. The canvas imports only the client-safe catalog projection, so no executor code ever reaches the browser bundle.

02

Contract

src/lib/flow/types.ts

One typed FlowGraph shared by everything: a list of nodes with typed params and a list of edges connecting output ports to input ports. zod validates it at every external boundary, and the graph codec is versioned, so flows saved under an older schema still parse. Because the contract is plain data, the same graph can be edited, exported as a template, validated at launch, and executed, without translation between representations.

03

Engine

src/lib/flow/engine.ts

An async generator that walks the graph in topological order and yields run events as it goes. It owns every execution guarantee on this page: dependency ordering, branch skipping, error halting, the cost ledger, the spend ceiling, the depth guard, and the dry-run gate.

04

Runtime

src/lib/run-service.ts + src/app/api

The hosted API around the engine: flow CRUD, streamed runs over SSE, cron-driven schedules, HMAC-signed webhooks, and, after launch, an x402-gated public run endpoint per agent plus machine-readable discovery documents. The runtime decides who may run what and whether money moves; the engine only ever executes what it is handed.

Execution semantics

What the engine guarantees on every run

Topological order. A node executes when every node feeding its inputs has finished. Edges are simultaneously the data flow and the execution order; there is no hidden shared state a node could read around them.

Branches skip, they don't fail. When a Branch or Switch activates one outgoing path, nodes on the inactive paths are marked skipped, a first-class node status distinct from error. A skipped node charges nothing and emits nothing downstream.

Errors halt the branch, not the run. When a node fails, everything downstream of it is halted so no later step runs against missing data, and nothing downstream is charged. Independent branches that don't depend on the failed node finish normally. The run then reports status error with every per-node outcome preserved.

The cost ledger. Every node execution is recorded to the run's ledger: node id, node type, terminal status, the USDC it cost, and whether that cost settled for real. The final run event carries the total. This is the same ledger the run dock renders live and the one you can audit after the fact.

The spend ceiling. Before every cost-bearing node, the engine checks one in-run ceiling: the minimum of the per-run cap (the RUN_COST_CEILING_USDC environment variable, $5 when unset) and the agent's remaining daily budget at run start. Crossing it aborts the run with an explicit cost-ceiling marker on both the failing node event and the final run event. Subflows and loops share the parent run's ceiling by reference, so a nested run cannot escape the budget by being nested, and a ceiling abort inside an iteration aborts the whole run instead of hiding in a per-item error list.

The depth guard. Subflow and Loop nodes run their inner flow one level deeper than the run that called them, and the engine refuses any run deeper than 16 levels. The guard exists to stop runaway recursion (a flow that loops itself), not to be designed against.

Dry-run is deny-by-default. The engine stubs any node declared cost-bearing or side-effecting before its executor runs. The gate fails safe: a node that requires a stub but doesn't declare one is refused outright rather than allowed to run for real, and an enumeration test pins the full catalog so a new node cannot ship without declaring its dry-run behavior. Which nodes are stubbed is listed per node in the node reference.

Tenant isolation at the graph boundary. A Subflow or Loop node can only load flows belonging to the same owner, and a flow that is missing returns the same error as a flow owned by someone else, so a graph cannot be used to probe for the existence of another tenant's private flows.

Streaming

Runs are event streams, not polling loops

The engine yields typed events as it executes, and the runtime forwards them verbatim as Server-Sent Events. The run dock in the studio and an API caller watching a run consume the identical stream:

run:start    { runId, at }
node:start   { runId, nodeId, nodeType }
node:log     { runId, nodeId, level: "info" | "error", msg }
node:done    { runId, nodeId, nodeType, outputs, costUsdc }
node:error   { runId, nodeId, nodeType, error, costCeilingExceeded? }
run:done     { runId, totalCostUsdc, status, abortedReason? }

Two details worth noticing. Cost arrives per node, on each node:done, so a watcher sees spend accumulate in real time rather than discovering it in a summary. And a cost-ceiling abort is distinguishable from an ordinary failure on both the node event and the final run:done, so a client can tell “this step broke” from “this run ran out of budget” without parsing error strings.

Versioning & promotion

Drafts change, versions don't, Live points at a version

Every flow keeps one mutable draft and any number of immutable saved versions. Deployment is environment-based, like software: you promote a saved version to the Test environment, watch it run, and only then promote to Live. Promotion is deliberate on purpose: the API requires a typed confirmation (PROMOTE TEST or PROMOTE LIVE) and pins the exact version being deployed by hash, so a concurrent edit cannot swap the graph between your approval and the deploy.

What Live runs is therefore exactly what you promoted: editing the draft after promotion changes nothing in production until you promote again. Version pinning extends through subflows, and the Live path refuses graphs containing the Connector Lab's simulation-only API Operation node, the same rule launch validation enforces.

Settlement posture

Money is opt-in at every layer

Settlement defaults to dry-run globally, per agent, and per request: no USDC moves until an agent's creator explicitly enables it, and an explicit dry-run request always stays free. On the selling side, a priced live call is verified and settled on Base before the flow executes, and the response's settled field is true only when a real payment settled. The caller's view of that handshake is in API for callers; the money model end to end is in Payments.