Skip to content

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.

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.

ValueShapeWhere from
Webhook URLhttps://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 with NEXT_PUBLIC_, PUBLIC_ or VITE_ — 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.
ActionWhat happens
EnableIssues a token and the URL starts working
Rotate tokenIssues a new token. The old URL 404s immediately. Use this if it leaks
DisableThe 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.

Terminal window
curl -i -X POST "$MINION_WEBHOOK_URL" \
-H 'content-type: application/json' \
-d '{"source": "chrome-extension", "url": "https://example.com/x/status/123"}'

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.

HeaderRequiredNotes
Content-Type: application/jsonYesMissing it gives 415. application/json; charset=utf-8 is fine
Anything else—Not read

POST is the only method; others return 405.

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.

ConstraintDetail
Top-level typeOne JSON object. Arrays, null, numbers and strings give 400
Empty body400. Send {} when you have nothing to pass
Size1MB (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.

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).

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.

StatusExample errorMeaning / what to do
400Request body must be a JSON objectAn array, a scalar or an empty body was sent
400Invalid JSON bodyMalformed JSON
400Payload violates start input contract "X"violations[] has the specifics; fix the shape
400Payload violates start outgoing edge contractsSame, for the contracts on start’s outgoing edges
400DAG workflow has no version / DAG workflow has empty graphThe workflow is unsaved or empty; tell its author
400No PM minion assigned to projectThe project has no minion with the PM role
404Not foundWrong token / webhook disabled / placement inactive. The three are indistinguishable (so tokens cannot be probed)
409Project is archivedThe project is frozen; only a person can unfreeze it in HQ
413Payload exceeds 1000000 bytesOver 1MB
415Content-Type must be application/jsonMissing header
500Internal server errorTransient 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.

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.

CallerWorks
A browser extension’s background / service worker (with the HQ origin in host_permissions)Yes
A server, CLI, CI job or minionYes
A web page’s frontend, directlyNo — go through your own server

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.

  • 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.
  • Suspect 404 first. 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 same 404.
  • 201 but 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.
  • 400 with violations is a shape problem: the field names, types and missing entries are spelled out for you.
WayPayloadNotes
ManualEntered on screenThe run button in HQ
Schedule (cron)Always {}5 minute minimum interval; fired by the project’s PM minion
WebhookThe JSON object you POSTThis page