Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

Execution model

How Axiom runs a flow: immutable compiled artifacts, unary vs pipeline (SSE) invocation, the Executions list and detail pages, and durable resume after worker failure.

View as Markdown

Every run of a flow is an execution: the platform compiles the flow into an immutable compiled artifact, assigns an execution ID when the request is accepted, and records durable checkpoints as each node completes. A flow runs in one of two modes — unary (one input message in, one result out) or pipeline (a stream of frames delivered over Server-Sent Events) — and either way the run survives infrastructure failure: if a worker dies mid-run, a replacement resumes from the last checkpoint instead of starting over.

Flows run as compiled artifacts

Invoking a flow never executes an editable draft — it always executes a compiled artifact. Compilation validates that every edge connects type-compatible messages, resolves every node placement, fixes the entry node and terminal node, and freezes the execution mode (unary or pipeline) into the artifact. An artifact is immutable: editing the flow and compiling again produces a new artifact with a new ID.

The artifact ID is the graph_id you pass when invoking the flow over HTTP. In the editor, the flow inspector's API section shows the flow's invoke endpoint, and its Use via API (curl) action compiles the current canvas and produces a ready-to-paste curl command with the compiled graph_id filled in. That graph_id is pinned to the version of the flow that was compiled — after editing the flow, reopen the dialog to get an updated command. The same section's Open interactive docs button opens a live API reference generated for that exact artifact (see Use the interactive API docs).

Workers cache compiled artifacts by ID, so repeat invocations skip graph resolution entirely.

Unary invocation

A unary flow handles one input message and produces one result per invocation. This is the default mode for flows that contain no pipeline nodes. Invoke it with POST /invocations/v1/flows/invoke on your deployment's origin, authenticated with an API key created under Console → API Keys (the key must belong to the same account that owns the flow):

# Replace the origin and graph_id with the values from "Use via API (curl)"
curl -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"graph_id": "01JX3Z9KQ4WV8RT2M5XB7CDEFG", "input": {"name": "Ada"}, "wait": true}'

The request body fields:

  • input — a JSON object matching the entry node's input message. The platform converts it to protobuf for you; callers never deal with binary serialization.
  • wait — when true, the call blocks until the flow completes and the response carries result, with the terminal node's output decoded back to JSON in result.output. The default wait timeout is 30 seconds; set timeout_seconds to override it. When false, the flow runs asynchronously and only execution_id is returned.
  • webhooknot currently supported. Asynchronous webhook delivery was removed along with the egress-delivery service, so a request that includes a non-empty webhook.url is rejected with HTTP 400 (webhook delivery not supported) rather than silently accepted and then dropped. For asynchronous runs, omit webhook and poll the execution result (or use wait: true).
  • config_id — optional flow config profile; when omitted the default hierarchy applies (tenant default → flow default). See Flow configs.

A successful wait: true response (HTTP 202):

{
  "accepted": true,
  "execution_id": "0af7651916cd43dd8448eb211c80319c",
  "result": {
    "output": { "greeting": "Hello, Ada!" },
    "success": true,
    "completed_at": 1780000000
  }
}

The execution_id identifies this run everywhere — the Executions pages, the events API, and distributed traces all reference it.

Pipeline invocation streams frames over SSE

A pipeline node is a streaming generator: declared with type: pipeline in axiom.yaml, it emits a sequence of output frames instead of a single output message. A flow that contains any pipeline node always runs in pipeline mode — frames flow continuously between nodes, and results stream back to the caller progressively.

Pipeline flows compile to a pipeline artifact and must be invoked on the SSE endpoint — wait applies only to unary invokes:

# -N disables curl's buffering so frames render as they arrive
curl -N -X POST 'https://api.axiomide.com/invocations/v1/flows/invoke/stream' \
  -H "Authorization: Bearer $AXIOM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"graph_id": "01JX3ZAB2PIPE8RT2M5XB7CDEF", "input": {"name": "Ada"}}'

The response is a Server-Sent Events stream (Content-Type: text/event-stream). Each event is one frame:

data: {"execution_id":"0af7651916cd43dd8448eb211c80319c","frame_index":0,"payload":{"greeting":"Hello, Ada!"},"is_final":false,"success":true}

data: {"execution_id":"0af7651916cd43dd8448eb211c80319c","frame_index":1,"is_final":true,"success":true}

payload is the flow output for that frame, decoded to JSON using the terminal node's output message. The stream ends when a frame arrives with is_final: true; on failure the final frame carries an error string instead of a payload. If no final frame arrives within the timeout (30 seconds by default, timeout_seconds to override), the stream closes with a final error frame.

Running a flow from the editor

The Run button sits at the bottom-center of the editor canvas and opens the Run Graph dialog. The dialog shows the flow's type — "Unary — single request / response" or "⚡ Pipeline — streams multiple frames via SSE" — with a Switch action to toggle between them. Switching is disabled when the flow contains a pipeline node, because a pipeline node's streaming frames cannot be delivered through a unary run.

Fill in the entry node's input fields and press ▶ Run. Unary runs also offer a Debug Mode toggle to watch the execution in real time node by node (see Debug a flow). Every run started from the editor is recorded as an execution, exactly like an API-triggered run.

The executions list

Click Executions in the top header to open /executions: one row per execution, newest first, with Status, Started, Duration, Flow, and Execution columns. Filter by status, by flow, or by date range. The same data is available over HTTP at GET /invocations/v1/executions.

An execution's status is one of:

StatusMeaning
QueuedAccepted, not yet picked up by a worker
RunningA worker is executing nodes
Paused (HITL)Waiting on a human-in-the-loop approval
Waiting on gateWaiting for parallel branches to converge at a gate
Debug pausedStopped at a breakpoint in a debug session
CompletedTerminal node finished; result available
FailedA node failed and the flow did not complete
CompensatingRunning compensation steps after a failure
CompensatedCompensation finished draining; the flow ended rolled back
CancelledStopped by an explicit cancel request

A running execution can be cancelled with DELETE /invocations/v1/flows/{execution_id}; cancellation takes effect immediately, even if the flow is blocked inside a long-running node.

Execution detail

Selecting an execution opens /executions/<execution-id>: a read-only canvas that replays the run on the flow's topology, a timeline scrubber to step through it, and a header showing the status plus Started, Updated, Completed, and Duration timestamps. Six tabs break the run down:

  • Timeline — the run's events in time order, driving the scrubber.
  • Events — the persisted per-node event log, filterable by node, including NODE_RETRY_SCHEDULED/STARTED/EXHAUSTED events with an "attempt N/M" chip on the row. Also available at GET /invocations/v1/executions/{id}/events.
  • Checkpoints — the durable checkpoint written after each completed node, including its recorded output. Also available at GET /invocations/v1/executions/{id}/checkpoints.
  • Pauses — human-in-the-loop pauses and how each was resumed.
  • Branches — gate activity for parallel branches.
  • Output — the flow's final output (or failure), decoded against the terminal node's output message.

When any node in the run retried, a retry rollup banner appears below the header, above the tabs: a per-node summary such as "3 attempts, succeeded on #3" or "5 attempts, failed after exhausting retries," derived from the run's own event log. This is a run-level summary distinct from the per-event "attempt N/M" chip on individual rows in the Events tab above — the banner tells you at a glance which nodes retried and how the retries resolved without opening the Events tab and reading the log; it's hidden entirely when nothing in the run retried.

The detail page is fed entirely from persisted history, so it renders the same run identically on every reload — including after the run finished.

Durable resume

After every completed node, the platform durably records a checkpoint containing that node's output before moving on. Checkpoints are what make an execution survive infrastructure failure: if the worker executing a flow dies mid-run, the platform redelivers the run to a replacement worker, which loads the checkpoint history, skips every node that already completed, and re-runs only the node that was in flight when the worker died. The execution keeps its ID and completes normally — callers waiting on a result or an SSE stream do not need to retry.

Checkpoints are retained for 30 days by default and are visible on the Checkpoints tab of the execution detail page. Large node outputs are stored out-of-band and rehydrated transparently when a resumed run (or the detail page) needs them.

Pauses are durable the same way: an execution in Paused (HITL) or Waiting on gate holds its state indefinitely — across worker restarts — until it is resumed or times out per its configured policy.

Retry and backoff

A node can carry a retry policy (see Configure per-node retry and backoff): on a retryable failure, instead of ending the execution the platform durably schedules another attempt and releases the worker slot in the meantime — the wait never blocks a worker, which is what lets a retry back off for minutes at a time without starving other flows. A node with a retry policy carries a 🔄 badge on the canvas, and a node currently backing off shows a live retrying state with a countdown to its next attempt.

Attempts, backoff, and timeouts

Each attempt is durably counted against the node's (execution_id, step_number) — the same identity a resumed execution replays after a worker crash, so a crash mid-backoff picks the attempt count back up rather than resetting it. The wait between attempts grows exponentially up to a configured ceiling, bounded by two independent timeouts: a per-attempt timeout (how long any single attempt may run) and an overall timeout across every attempt (ScheduleToClose) — once the overall budget is spent, retries stop even with attempts remaining.

Retryable vs. non-retryable failures

Not every failure is worth retrying. A transport-level failure (a dropped connection, a timeout) is retryable by default; a deterministic business failure — the node ran and decided the input was invalid — is non-retryable by default, because retrying it just reproduces the same failure. A policy can narrow this further with an explicit non-retryable list, but it can never widen it to force a retry of a business failure.

Scope

Retry applies to unary nodes only in this release. Pipeline (streaming) nodes and heartbeat-based liveness checks are explicit non-goals.

Failure edges and typed errors

A node's terminal failure no longer has to end the execution. If the node has an outgoing failure edge (an edge authored to fire only on failure, after any retry policy exhausts its attempts), the worker builds a typed error payload — a closed error code, a safe message, and structured detail — and routes it down that edge exactly like a normal output, without cancelling the flow. The receiving node runs as usual, and if its own output reaches the flow's terminal node, the execution completes instead of failing. On the canvas, a node with a failure edge shows a second, red output handle so the branch is visible next to its normal outputs.

Ordering is fixed and deterministic: a retry policy's attempts exhaust first, then a failure edge (if one exists) gets a chance to route the error, then join-tolerance on any downstream fan-in gate, and only then does the platform fall back to cancelling the flow. A failure a failure edge handles is never also recorded as a failed join input — the two mechanisms never double-handle the same failure.

Compensation (saga rollback)

Some nodes declare a paired compensation node — the undo for whatever that node did. The platform tracks these as a stack, pushed just before a node with a compensation link runs and popped once it succeeds. When an execution reaches a terminal failure with a non-empty stack, the platform sets its status to Compensating and unwinds the stack LIFO — most recently succeeded step compensated first — dispatching each compensation node in turn and popping its frame only after that node itself succeeds.

Draining the whole stack moves the execution to the Compensated terminal status; a compensation node that itself fails stops the drain and the execution ends Failed instead, leaving the remaining frames recorded for inspection. Compensation survives a worker crash the same way ordinary execution does: a replacement worker reloads the stack and resumes the drain from the exact frame that was in flight, never re-running a compensation that already succeeded.

An author can also force a rollback explicitly by routing a failure edge into one marked as rollback-forcing — useful when a node's failure should undo prior work even though the author is otherwise handling that failure on the canvas.