Skip to main content
Trigger a workflow from your own code with a single call to POST /workflows/run, then open a Server-Sent Events stream to watch the run unfold node by node. The call returns immediately with a run_id while the workflow executes in the background; you read its output by listening on that run_id. This is the programmatic equivalent of pressing Run in the builder, and it is the same path the official SDKs use under the hood. This page is the exhaustive reference for the run endpoint: every request field, the full response, the event stream, the credit impact, and every error — in cURL, Python, and JavaScript. For an end-to-end narrative walkthrough, see the Run a workflow guide. For the request lifecycle that applies to all endpoints, see the API overview.
The run endpoint mounts at the root of the API host — there is no /v1 or /api path segment. The canonical base URL is https://api.modulex.dev. See Base URLs & versioning.

Before you run

Three things must be true before a run succeeds:
1

You have an API key and an organization id

Create an mx_live_* API key and note the organization the workflow belongs to. Both go on every request.
2

You hold the owner or admin role

POST /workflows/run requires the owner or admin organization role. The member role is retired — a non-admin caller gets 403.
3

The workflow has a live deployment

Running a saved workflow by workflow_id loads the schema from its live deployment. If none is active, the call returns 400 and asks you to deploy first. (Ad-hoc runs that pass an inline definition skip this requirement.)

Authentication

Every request carries two headers, exactly as elsewhere in the API: Authentication failures surface before the handler runs: 401 (with WWW-Authenticate: Bearer) for a missing or invalid key, and 403 for a caller who is not an org admin or owner. See Authentication and Org context & X-Organization-ID.
The auth header is Authorization: Bearer, not X-Authorization. Neither SDK nor the backend reads X-Authorization. See Auth model: JWT vs API key.

The endpoint

endpoint
Starts a workflow run in the background and returns a run_id you can stream. Requires the owner or admin role. Subject to the billing admission gate.

Choosing an execution mode

The body must select exactly one source for the workflow to run. The three modes are resolved in this order:
string (UUID)
Saved-workflow mode. Runs the schema from the workflow’s live deployment. The request input overrides the deployment’s default input; the request config is merged over the deployment config. If the workflow has no active deployment, the call returns 400. This is the most common production mode.
object (WorkflowDefinition)
Ad-hoc mode. Runs an inline WorkflowDefinition graph that is never saved. The run is flagged is_ad_hoc: true. Pass attribution_workflow_id to attach the run to a saved workflow’s run history without switching to saved-workflow mode.
string
System mode. Runs a named built-in workflow shipped with ModuleX. In this mode both input and config are required (400 if either is missing).
The legacy “LLM mode” — a request carrying only an llm config and no workflow — has been removed and now returns 410 Gone. For a tool-using chat agent with no workflow, use the Assistant (POST /assistant/chat) instead.

Request body

string (UUID)
Saved-workflow mode. Mutually exclusive with workflow and system_workflow. Requires a live deployment.
object
Ad-hoc mode: an inline WorkflowDefinition (metadata, config, state_schema, nodes[], edges[], entry_point). See the workflow engine for the schema. A malformed definition surfaces as 500, not 422 (see Errors).
string
System mode: the name of a built-in workflow. Requires input and config.
object
The run’s input state, keyed by your state_schema fields (defaults to an empty object). Values are read by the entry node and become resolvable as `{{node_id.field}}` references in downstream nodes. In saved-workflow mode this overrides the deployment’s default input.
object
Per-run execution config (defaults to an empty object). Recognized keys are below; in saved-workflow mode this is merged over the deployment config.
boolean
default:"true"
Echoed back in the response. The run always executes in the background and is observed via the listen stream; this flag does not toggle token streaming (that path was removed).
boolean
default:"false"
When true, no chat record is created: chat_id is null, thread_id is a fresh UUID, and human_message / ai_message are null in the response. Use this for fire-and-forget API runs you do not want to surface in chat history.
boolean
default:"false"
When true, the chat created for this run is private rather than organization-visible.
string (UUID)
Ad-hoc mode only. Stamps the run record with this workflow id so the run appears in that workflow’s Runs panel, without loading its deployment. Used by the builder’s “Run” on a live, unsaved canvas.
The endpoint reads no Idempotency-Key header for run dedup — it mints its own run_id as the internal reservation key. The Python SDK accepts an idempotency_key argument and sends the header, but it is a no-op for run de-duplication. See Errors & retries.

Trigger a run

The following triggers a saved workflow by id, passing an input and a per-run recursion_limit. Authenticate with Authorization: Bearer + X-Organization-ID.
In the JavaScript SDK, request fields are written in camelCase (workflowId) and converted to snake_case on the wire, but responses stay snake_case — read run.run_id, never run.runId. The Python SDK is snake_case in both directions. See the SDK parity matrix.

Response

The call returns 200 immediately, before the workflow finishes — the status is running, and you track progress on the stream. (Resume reuses the same identifiers; it does not start a second run.)
Response
string
Always running on a successful trigger — the run is asynchronous. Terminal status is observed on the stream (done / error / cancelled) or read back from run history.
string
The per-execution identifier. Use it to stream, cancel, and look up history. A resume reuses this run_id. This is not the run record’s id (returned by list/get) and not the thread_id. See the three run-id identities.
string
The conversation/checkpoint thread. When ephemeral is false, thread_id == chat_id. When ephemeral is true, thread_id is a fresh UUID and chat_id is null. Pass it to GET /workflows/state/{thread_id} to inspect a paused run, or to POST /workflows/resume/{thread_id}.
string | null
The chat this run is attached to, or null for an ephemeral run.
boolean
Echoes the request ephemeral flag.
boolean
Echoes the request stream flag.
string
The workflow’s name (from the deployment, system file, or inline metadata).
string
The workflow’s version string.
string
Where the schema came from: database (saved-workflow live deployment), request (ad-hoc inline), or system:<name> (system workflow).
number
Time spent setting up the run before responding (not the run’s total duration).
object | null
The chat message envelope created for the trigger, or null when ephemeral (or if chat creation failed).
object | null
The placeholder AI message envelope whose running_status advances as the run streams, or null when ephemeral.
string
A human-readable hint pointing at the listen endpoint.

Stream the run

The trigger returns instantly; you observe the run by opening an SSE stream on its run_id.
endpoint
A text/event-stream of run events. Multiple clients can listen to one run_id concurrently. On (re)connect, the recent event history (a one-hour buffer) is replayed in order, then the stream tails live. Requires the same auth and owner/admin role.
The stream is data-only: every frame is `data: {json}\n\n` with no SSE event: line. The discriminator is the type key inside the JSON. The SDKs normalize this so you switch on one field — evt.type (JavaScript) or event.event (Python).

Run event types

The executor publishes these frames. Read the wire shapes below — within one run stream some payloads are wrapped (`{type, data:{…}}`) and some are flat (`{type, …fields}`). Consume defensively; the published wire shape, not the typed SDK models, is authoritative.
interrupt does not close the stream — it leaves it open so you can render the human-in-the-loop prompt, then resume. Do not treat the quiet that follows an interrupt as completion. The server closes a run stream only on done or error. See Human-in-the-loop (HITL) resume and the interrupt node.
A raw stream for a run that completes without pausing looks like this:
Raw SSE frames
For the full SSE frame format, the heartbeat and one-hour replay model, and how the SDK listen() generators consume it, see SSE run streaming and Streaming & human-in-the-loop in the SDKs.

Resume a paused run

If a run hits an interrupt node, it pauses and waits for a value. Post the answer to POST /workflows/resume/{thread_id} with the run_id and a resume_value, then re-listen on the same run_id (workflow resume reuses the run id — unlike Composer/Assistant chat resume, which mints a new one).
resume_value and run_id are required (400 if missing), as is either workflow_id (re-supply the deployed workflow) or an inline workflow. Resuming does not charge a second credit — the gate’s reservation is keyed on the reused run_id. See Human-in-the-loop (HITL) resume.

Cancel a run

POST /workflows/cancel/{run_id} requests cancellation of a running or interrupted run. Cancellation is graceful — the current node finishes, then the executor stops between nodes and emits a cancelled frame.
Cancelling a run that is not running or interrupted returns 400. Closing the SSE connection alone does not stop the run — you must call cancel to terminate it server-side.

Read run history

After a run ends, its durable summary is queryable. GET /workflow-runs lists runs (newest first, org-scoped, filterable by workflow_id / status / trigger_type); GET /workflow-runs/{run_pk} returns the full record including input_snapshot and output_summary.
GET /workflow-runs/{run_pk} takes the run record’s id (returned by list/get), not the executor run_id. Passing a run_id returns 404. The list rows expose both fields — id is the run record’s id, run_id is the execution id. See the three run-id identities.
cURL
The durable status uses succeeded for a completed run, whereas the live SSE terminal frame is done — the same outcome under two names. Durable statuses are pending, running, succeeded, failed, cancelled, interrupted, and skipped.

Credit impact

A workflow run consumes managed credits. Two things to know:
  • One run credit per logical run. Each run is charged a flat RUN_CREDIT of 1 credit, recorded exactly once. A resume reuses the run’s reservation and is not charged again. Token usage by managed LLM nodes and managed knowledge retrieval is metered on top of the run credit. Bring-your-own-key usage is not credited.
  • The gate runs before any work. Admission is checked before the run record or background task is created (reject-before-write). A denied request creates no run and no chat rows. API-key runs additionally consume a sync_exec run-rate-limit slot; editor (JWT) runs do not.
For what a credit is and exactly what consumes credits, see Credits & metering. For the gate and its limits, see Usage gating & limits.

The billing gate

POST /workflows/run sits behind the live billing admission gate. When your organization is over a limit, the gate denies the run and returns a flat DenialEnvelope`{code, layer, key, current, limit, reason}` — with no detail wrapper. The HTTP status depends on the layer.
This flat envelope is specific to the gated run / Composer / Assistant / managed-knowledge surfaces. Plain CRUD routes (creating, listing, or deleting a workflow) instead return the FastAPI `{"detail": "…"}` shape. A 402/403/429 on the run endpoint is a DenialEnvelope; the same status on a CRUD route is not. See Errors & status codes.
402 DenialEnvelope
The SDKs surface these on the run() call. The Python SDK maps them to typed exceptions — CreditExhaustedError (credit), WalletError (wallet), QuotaExceededError (quota), and base BillingError for the rate layer — while the JavaScript SDK exposes .code, .layer, and .reason on a base ModulexError (it has no dedicated 402 class). Branch on these in your error handler. See Errors & retries.

Errors

Beyond the billing-gate responses above, the run endpoint can return:
error
Missing X-Organization-ID; no execution source provided (none of workflow_id / workflow / system_workflow); missing input or config in system mode; or the saved workflow has no active deployment (`"Workflow has no active deployment. Deploy the workflow first…"`). Shape: `{"detail": "…"}`.
error
Missing or invalid API key. Carries WWW-Authenticate: Bearer. Shape: `{"detail": "…"}`.
error
The caller is not an owner/admin, or an org-scope mismatch on the workflow. (A quota-layer billing denial is also 403 but uses the DenialEnvelope shape.) Shape: `{"detail": "…"}`.
error
A workflow_id or system_workflow that does not exist, or that belongs to another organization — the same 404 covers both (no existence leak). Shape: `{"detail": "…"}`.
error
The removed LLM-only mode (a request with only an llm config). Use the Assistant instead. Shape: `{"detail": "…"}`.
error
A malformed inline workflow schema surfaces as 500 (`"Failed to start workflow: …"`), not the usual 422, because the run body is parsed as an untyped object. Standard 422 validation arrays apply to typed query parameters elsewhere.
error
A wrapped failure (`"Failed to start workflow: …"`) or the catch-all `{"detail":"An unexpected internal server error occurred."}`.
For the three error-envelope shapes and which surface emits each, see Errors & status codes. For rate limiting specifically, see Rate limiting.

Worked example: trigger, stream, resume

A complete loop — trigger a run, stream it, answer an interrupt if one arrives, then read the final state. This is the shape most production integrations follow.

Run a workflow (REST + SDK)

The end-to-end guide: authenticate, run, and stream in three languages.

API overview & request lifecycle

Base URLs, content types, and how every operation is shown three ways.

SSE run streaming

The raw frame format, full event taxonomy, heartbeat, and replay model.

Usage gating & limits

The billing admission gate and its 402/403/429 DenialEnvelope responses.

Human-in-the-loop (HITL) resume

Pause and resume semantics for the interrupt node.

Deploy & versions

Create the live deployment that saved-workflow runs load from.