Triggering a workflow by webhook
A DAG workflow can hand you a URL that starts it with a single POST. A script on your machine, an internal service, or the “send” button in a browser extension can all run the workflow through it.
This page is deliberately self-contained: hand it over and the integration can be written from it
alone (whether the reader is a person or a coding assistant). A plain-text version for language
models lives at /llms-dag-webhook.txt.
The whole thing in three lines
Section titled “The whole thing in three lines”Place a workflow in a project and that placement can issue a URL containing a secret token. POST a JSON object to it and the body becomes the start node’s output verbatim, reaching the downstream nodes in exactly that shape. The token in the URL is the only credential — no headers, no signature.
Values you need before you start
Section titled “Values you need before you start”| Value | Shape | Where from |
|---|---|---|
| Webhook URL | https://minionworkspace.com/api/dag/webhooks/<token> | Open the workflow in its project, then Webhook Trigger → Enable → Copy |
- The host is the same origin you open Minion on. Neither the workspace id nor the project id
appears in the path. The path is only
/api/dag/webhooks/<token>, and the token decides where the request lands. - There is one URL per workflow × project. If the same workflow is placed in two projects, the URLs differ, and the URL decides which project’s members, roles and context the run uses. The caller cannot choose the project.
- The token is 32 random bytes (base64url, 43 characters). The whole URL is the credential, so
keep it in an environment variable (for example
MINION_WEBHOOK_URL). Never prefix it withNEXT_PUBLIC_,PUBLIC_orVITE_— that embeds it in the JavaScript you ship to browsers. - The screen masks the token portion, but Copy always copies the full URL. It is not a show-once secret: if you lose it, take it from the screen again.
Enabling, stopping, reissuing
Section titled “Enabling, stopping, reissuing”| Action | What happens |
|---|---|
| Enable | Issues a token and the URL starts working |
| Rotate token | Issues a new token. The old URL 404s immediately. Use this if it leaks |
| Disable | The URL stops working, and the token is discarded |
Do not use Disable as a pause. Enabling again issues a different URL, so every caller has to be reconfigured.
The request
Section titled “The request”curl -i -X POST "$MINION_WEBHOOK_URL" \ -H 'content-type: application/json' \ -d '{"source": "chrome-extension", "url": "https://example.com/x/status/123"}'Authentication
Section titled “Authentication”There is no auth header. No Authorization: Bearer ..., no X-...-KEY, no signature. The token
in the URL is the only credential. Extra headers are neither read nor rejected.
Because the URL itself is the key, keep it out of logs, error reports, Referer and screenshots.
Headers
Section titled “Headers”| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | Yes | Missing it gives 415. application/json; charset=utf-8 is fine |
| Anything else | — | Not read |
POST is the only method; others return 405.
The body
Section titled “The body”There are no fixed fields. No title / body / url envelope is defined. Use whatever keys
and types you like — strings, numbers, booleans, arrays, nested objects — and do not wrap them
({"payload": {...}} is not a thing here). The object you send becomes the start node’s
output_data verbatim, and downstream nodes read it in that shape.
| Constraint | Detail |
|---|---|
| Top-level type | One JSON object. Arrays, null, numbers and strings give 400 |
| Empty body | 400. Send {} when you have nothing to pass |
| Size | 1MB (checked against Content-Length; over it gives 413) |
The receiving side may have fixed the shape
Section titled “The receiving side may have fixed the shape”If the workflow’s start node declares an input_contract, or
the edges leaving start carry contracts, the body must satisfy that type — otherwise the run is
rejected with 400 before a single node executes. The response’s violations names each field and
how it differs.
A caller cannot tell in advance whether a contract exists. Ask whoever built the workflow whether start has one and, if so, for the field names and types. With no contract, any object passes.
The response
Section titled “The response”Success
Section titled “Success”HTTP/1.1 201 Created
{ "execution_id": "3f0c…", "root_nodes": 1, "total_nodes": 7 }201 means accepted and started, not finished. The workflow itself runs asynchronously on the
minions afterwards.
There is no API for reading progress or results from outside. execution_id identifies the run
in the HQ screens, and the endpoint that returns a run’s state requires a logged-in session, so the
webhook token cannot read it. If the caller needs the outcome, have the workflow call your
service (a skill or script node at the end that POSTs to you).
Failure
Section titled “Failure”The body is always {"error": "<one English line>"}, plus a violations array for contract
violations. error is short enough to drop straight into a button title or a toast.
| Status | Example error | Meaning / what to do |
|---|---|---|
400 | Request body must be a JSON object | An array, a scalar or an empty body was sent |
400 | Invalid JSON body | Malformed JSON |
400 | Payload violates start input contract "X" | violations[] has the specifics; fix the shape |
400 | Payload violates start outgoing edge contracts | Same, for the contracts on start’s outgoing edges |
400 | DAG workflow has no version / DAG workflow has empty graph | The workflow is unsaved or empty; tell its author |
400 | No PM minion assigned to project | The project has no minion with the PM role |
404 | Not found | Wrong token / webhook disabled / placement inactive. The three are indistinguishable (so tokens cannot be probed) |
409 | Project is archived | The project is frozen; only a person can unfreeze it in HQ |
413 | Payload exceeds 1000000 bytes | Over 1MB |
415 | Content-Type must be application/json | Missing header |
500 | Internal server error | Transient failure |
Retrying a 4xx gives the same answer, so fix it first. Only 5xx and timeouts are worth
retrying — read the next section before you do.
Deduplication
Section titled “Deduplication”There is none. The Idempotency-Key header is not read, and no body field serves that purpose.
POST the same body twice and two runs happen.
Retrying after a timeout is risky because the run may already exist even though no 201 came
back. If you need protection, pick one:
- Put a unique value in the body (
"source_id": "...") and let the workflow drop what it has already handled - Do not retry automatically — surface the failure and let the user press the button again (usually enough for an extension button)
No CORS headers are returned. A POST with Content-Type: application/json always triggers a
preflight, so calling it directly from a web page’s JavaScript is blocked by the browser. That is
intended: the whole URL is a credential and does not belong in code you ship to browsers.
| Caller | Works |
|---|---|
A browser extension’s background / service worker (with the HQ origin in host_permissions) | Yes |
| A server, CLI, CI job or minion | Yes |
| A web page’s frontend, directly | No — go through your own server |
Rate limits
Section titled “Rate limits”The application applies none. But each POST really does start a workflow and consume (billable) minion compute time, so a double-clicked button becomes a pile of runs. Stop double submits on your side — disable the button while a request is in flight.
Images and other files
Section titled “Images and other files”- This endpoint takes JSON only. No multipart, no binary.
- Passing a URL is the straightforward option. It keeps the request small, and the minion running the workflow can fetch it — provided the URL needs no authentication. An image that requires a login is out of reach for the minion too.
- Base64 works but counts against the 1MB cap. Base64 inflates by roughly 1.37×, so the practical ceiling is an image of about 700KB. Anything larger should be a URL, or go through Drive first.
- The body is stored as the run’s record, so base64 inflates the execution history by the same amount.
When it does not work
Section titled “When it does not work”- Suspect
404first. Did you enable it, is this the URL from after the last rotate, and is the workflow’s placement active? All three collapse into the same404. 201but apparently nothing happens — check the executions list in HQ. The run exists and its downstream nodes are either waiting to be claimed by a minion or have failed.400withviolationsis a shape problem: the field names, types and missing entries are spelled out for you.
The other ways to start a workflow
Section titled “The other ways to start a workflow”| Way | Payload | Notes |
|---|---|---|
| Manual | Entered on screen | The run button in HQ |
| Schedule (cron) | Always {} | 5 minute minimum interval; fired by the project’s PM minion |
| Webhook | The JSON object you POST | This page |