Workflows can be triggered via HTTP webhooks. The standard execute endpoint returns JSON, and the streaming variant returns Server-Sent Events (SSE).
Endpoint
POST /api/workflows/{workflow_id}/execute
Replace {workflow_id} with the workflow UUID. The base URL is your Heym instance (e.g. https://app.heym.ai).
Request Method
Each workflow accepts one HTTP verb, chosen in the editor's cURL dialog: GET, POST, PUT, or DELETE. POST is the default, and every workflow created before this setting existed keeps it, so nothing that works today changes.
Calling a workflow with a different verb returns 405 Method Not Allowed with an Allow header naming the configured one:
$ curl -X POST "https://your-host/api/workflows/<workflow-id>/execute"
HTTP/1.1 405 Method Not Allowed
Allow: GETOne exception: requests carrying ?test_run=true skip the check. That is what the editor's Run button and the debug panel send, so a workflow set to GET stays testable from inside the product.
Bodyless methods
GET and DELETE carry no request body. Inputs come from the query string instead, and are read with $input.query.<name>:
curl -X GET "https://your-host/api/workflows/<workflow-id>/execute?name=Ada"The cURL dialog hides the request-body editor and drops the Content-Type header for these two methods.
Pair GET with an HTML output mapper to serve a page a browser can open directly.
Streaming Endpoint
POST /api/workflows/{workflow_id}/execute/stream
Use the streaming endpoint when you want incremental execution events instead of a single JSON response. See SSE Streaming for the event protocol and per-node start message configuration.
Request Format
The request body is passed as body to Input nodes. Heym accepts both request shapes:
- Raw JSON – e.g.
{"userId": "123", "action": "create"} - Wrapped format –
{"inputs": {...}, "test_run": false}
When using the wrapped format, inputs is extracted and passed as body. test_run bypasses rate limiting and response caching. You can also send ?test_run=true as a query parameter when posting raw JSON.
Request Body Modes
Each workflow has a webhook_body_mode setting that controls how the editor builds sample requests:
| Mode | Behavior |
|---|---|
| defined | The editor and cURL dialog shape the example body from the workflow's Input node fields and keep empty keys in the JSON payload. Existing defined-style cURL payloads keep working. |
| generic | The editor and cURL dialog use a free-form JSON body. The request body is passed through as-is so external apps can send their own fixed payload shape. |
The execution API stays backward compatible in both modes. Raw JSON and wrapped { "inputs": ... } requests are still accepted either way.
Headers and query params are available in expressions as $input.headers and $input.query. Header keys are lowercased.
Authentication
Configure per workflow in the editor (workflow settings). Auth is checked before execution.
| auth_type | Behavior |
|---|---|
| anonymous | No auth required. Anyone can call the endpoint. |
| jwt | Bearer token required. Accepts either a user session token (user must have workflow access) or a scoped Execution Token created for this workflow. |
| header_auth | Custom header must match. Set auth_header_key (e.g. X-API-Key) and auth_header_value. |
For header_auth, the request header value must exactly match the configured value. A user with workflow access can also call without the custom header when the request sends their token as Authorization: Bearer …, or when it is a run started from inside Heym. A link opened in the browser carries only the session cookie, so it does not count as that user and needs the custom header like any other caller. See Who counts as the caller.
For jwt, Execution Tokens are the recommended way to call workflows from external systems — they are scoped to a single workflow and can be revoked independently.
Response Caching
Configure cache_ttl_seconds per workflow. When set and greater than 0:
- Cache key: SHA256 hash of
workflow_id+body+query - Cache hit: Returns immediately with
status: "cached"and cached outputs - Cache set: Only after a successful execution (
status: "success") - Storage: Postgres-backed and shared across backend workers
| Setting | Effect |
|---|---|
cache_ttl_seconds = 0 or null | Caching disabled |
cache_ttl_seconds > 0 | Responses cached for this many seconds |
test_run requests bypass the cache (no read, no write).
When response caching is enabled in the cURL dialog, Clear cache evicts the current workflow's cached responses immediately.
Rate Limiting
Configure per workflow: rate_limit_requests and rate_limit_window_seconds.
- Scope: Per
workflow_id+ client IP (sliding window) - Exceeded: HTTP 429 with
Retry-Afterheader - Headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset
| Setting | Description |
|---|---|
rate_limit_requests | Max requests allowed in the window |
rate_limit_window_seconds | Window duration in seconds |
test_run requests bypass rate limiting.
Concurrency Limit and Queued Runs
A workflow can cap how many production runs execute at once. Turn on Concurrency limit in the workflow-level Properties panel and set Max concurrent runs. Owners and collaborators with edit access can change it. The limit is off by default.
While the limit is reached, new runs wait in a first-in, first-out queue. This covers every production entry point: this endpoint, the streaming endpoint, the Portal, the Quick Drawer, MCP and every trigger. The editor's own test runs always start right away and do not take a slot. Sending test_run=true without a signed-in session that can open the workflow does not skip the queue.
| Setting | Description |
|---|---|
concurrency_limit | Production runs allowed at once. 0 turns the limit off |
concurrency_max_queue | Optional. Runs allowed to wait at once. Empty means no limit |
concurrency_max_wait_seconds | Optional. A run that waits longer is recorded as expired. Empty means it waits as long as it takes |
202 Accepted
A call that starts right away behaves exactly as described under Response Format. A call that has to wait gets 202 Accepted at once, whatever X-Simple-Response says, with a Location header that points at its status URL:
{
"status": "queued",
"execution_id": "6f1c0c2e-...",
"status_url": "https://heym.example.com/api/workflows/{workflow_id}/executions/{execution_id}/status",
"queue": { "position": 2, "size": 5, "running": 1, "limit": 1 },
"status_token": "..."
}| Field | Description |
|---|---|
queue.position | This run's place in line, starting at 1 |
queue.size | Runs waiting right now |
queue.running | Runs holding a slot right now |
queue.limit | The workflow's concurrency limit |
status_token | Anonymous workflows only. Returned once, in this response |
Status URL
GET /api/workflows/{workflow_id}/executions/{execution_id}/status answers with the run's state. Call it with the same credential that started the run: a session or JWT, an execution token, or the workflow's custom header. An anonymous workflow has no credential to check, so send the status_token from the 202 in the X-Heym-Run-Token header. The token is never read from the query string. A wrong or missing token gets 404, the same as an unknown execution.
{
"execution_id": "...",
"workflow_id": "...",
"status": "success",
"queue": null,
"outputs": { "done": { "result": "ok" } },
"error": null,
"execution_time_ms": 5120
}status | Meaning |
|---|---|
queued | Waiting for a slot; queue shows where it stands |
running | Has a slot and is executing |
success / error | Finished; outputs and error are filled |
pending | Paused for human review |
cancelled | Cancelled from Execution History |
expired | Waited longer than concurrency_max_wait_seconds and never ran |
Poll every few seconds until the status is not queued or running. A queued run executes even if nobody polls.
Queue Full
When concurrency_max_queue is set and that many runs are already waiting, the call is turned away with 429 Concurrency queue is full and a queue_full entry in the run history. Nothing is queued.
Response Format
The endpoint returns JSON:
{
"workflow_id": "uuid",
"status": "success",
"outputs": { ... },
"execution_time_ms": 123
}| Field | Description |
|---|---|
workflow_id | Workflow UUID |
status | success, error, cached, or rate_limited (a run that has to wait for a concurrency slot gets the 202 response instead) |
outputs | Terminal node outputs. With a single JSON output mapper as the only terminal, the response body is that mapped object at the top level (default simple JSON responses). Otherwise includes Output and other terminals keyed by label. |
execution_time_ms | Execution time in milliseconds (0 for cached/rate_limited) |
On error, outputs may contain error details. A Throw Error node can set a custom HTTP status code (4xx/5xx).
Run with cURL Dialog
The Run with cURL toolbar button (top-right of the editor, cURL icon) opens a dialog that builds and copies a ready-to-run cURL command for the current workflow.
What It Does
| Field | Description |
|---|---|
| Request Body Mode | Choose Defined or Generic. Generic keeps the body as raw JSON instead of shaping it from Input fields. |
| Authentication | Choose Anonymous, JWT Token, or Header Auth. Sets the correct auth header in the command. When JWT Token is selected, the dialog shows an Execution Token manager — create or select a token to embed it directly in the command. |
| Header Key / Value | Visible when Header Auth is selected. Pre-filled from the workflow's auth settings. |
| Response Cache | Minutes to cache identical responses (0 = disabled). Changes are saved to the workflow. When enabled, Clear cache evicts cached responses immediately. |
| Rate Limit | Max requests per time window. Changes are saved to the workflow. |
| SSE Streaming | Switches the command to /execute/stream, adds --no-buffer, and exposes per-node node_start message settings. |
| Body | JSON request body. In Defined mode it is pre-filled from Input fields and keeps empty keys. In Generic mode it is a raw JSON editor. |
| Command | Read-only preview of the full cURL command. |
Click Copy cURL to copy the command to the clipboard.
Example
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-value" \
"https://app.heym.ai/api/workflows/{workflow_id}/execute" \
-d '{"message": "Hello"}'In Generic mode, nested payloads stay intact, for example:
curl -X POST \
-H "Content-Type: application/json" \
"https://app.heym.ai/api/workflows/{workflow_id}/execute" \
-d '{"event":{"user":{"id":"123"}}}'The dialog mirrors the authentication, body mode, caching, and rate-limit settings stored on the workflow — editing them here updates the workflow immediately.
SSE Streaming in the Dialog
Enable SSE Streaming when you want the generated command to follow execution in real time from your terminal.
- The endpoint changes from
/executeto/execute/stream - The command adds
--no-bufferandAccept: text/event-stream - Each non-sticky node appears in a list where you can:
- enable or disable the
node_startmessage - customize the start message text
- enable or disable the
node_completeevents are always emitted and include the full node output
Example:
curl -X POST --no-buffer \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Authorization: Bearer <your-execution-token>" \
"https://app.heym.ai/api/workflows/{workflow_id}/execute/stream" \
-d '{"message":"Hello"}'For the complete event protocol and examples, see SSE Streaming.
Trigger Source Tagging
Every execution stores a trigger_source label visible in Execution History. Set it by sending the X-Trigger-Source header with any value you choose:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Trigger-Source: my-service" \
"https://app.heym.ai/api/workflows/{workflow_id}/execute" \
-d '{"message": "Hello"}'The Copy cURL button in the editor dialog includes X-Trigger-Source: API automatically. You can replace API with anything meaningful — scheduler, rabbitmq, github-actions, etc.
If the header is not sent, trigger_source defaults to "API" and the run is labelled API in history. Override it with any value meaningful to your system — scheduler, github-actions, rabbitmq, etc.
Related
- Execution Tokens – Scoped JWTs for calling workflows from external systems
- SSE Streaming – Stream node events in real time via Server-Sent Events
- Triggers – All workflow entry points including webhook
- Workflow Structure – Nodes and edges
- Expression DSL – Access
$input.body,$input.headers,$input.query