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 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).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-runrecursion_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 returns200 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 itsrun_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.`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.
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 toPOST /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.
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.
cURL
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_CREDITof 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_execrun-rate-limit slot; editor (JWT) runs do not.
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.
402 DenialEnvelope
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."}`.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.Related
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.