Call a workflow’s webhook
Start a published workflow from any service with one HTTP call, and get its answer in the same call.
You need
- A webhook workflow
- A workflow that starts with the Webhook trigger: Start from a webhook.
- A token, once
- Only to read the address. Calling it needs none.
1.Get the address
The address is your host, then /api/webhooks/ and the workflow’s webhook token. It answers only once the workflow is published.
In the builder, the Webhook trigger shows it under Webhook URL (production). From the API, publishing answers webhookToken, and so does Get a workflow for a token that may run workflows.
Warning Anyone who has the address can start production runs. Keep it secret, or protect it with the trigger’s Authentication. To stop an old address, press Regenerate URL under it, or get a new one by API.
WEBHOOK_TOKEN=$(curl -s -X POST "$GORUNNER/api/workflows/$ID/publish" \
-H "Authorization: Bearer $GORUNNER_TOKEN" \
| jq -r .data.webhookToken)
HOOK="$GORUNNER/api/webhooks/$WEBHOOK_TOKEN"2.Call it
Send any method the trigger allows. No token: the address is the key. If the trigger’s Authentication asks for Basic Auth, a header or an HMAC signature, send that.
The workflow receives the request as its trigger data:
{{trigger.body}}- JSON as sent. Form fields as an object. Multipart fields too, with each file’s name, size and type. Anything else as text; nothing as
{}. {{trigger.query}}- The query string, as an object.
{{trigger.headers}}- Names in lowercase.
authorization,proxy-authorization,cookie,set-cookie,x-api-keyand the trigger’s own header are kept as[redacted]. {{trigger.method}}POST,GETand so on.
A body may be up to 5 MiB.
curl -i -X POST "$HOOK?source=crm" \
-H "Content-Type: application/json" \
-d '{"orderId": "A-1001"}'HTTP/1.1 201 Created
Content-Type: application/json
{"received":"A-1001"}This workflow ends with a Return Response step: status 201, and a JSON body built from {{trigger.body.orderId}}.
3.Read the answer
GoRunner waits up to 30 seconds for the run, then answers:
- With Return Response
- Its status, body and headers. The response type sets
Content-Type, over any you gave. With After responding set to Keep running, you get it as soon as that step runs. A header that would act on GoRunner’s own address, such asSet-Cookie, fails the step, and you get500. Check a definition warns of it. - Without one
200and the last step’s output, with the run’s ID inX-Run-Idand its state inX-Run-Status.{}if no step gave output.- Still running
202withrunId,statusand a message: after 30 seconds, when the run waits on a Delay or a person, or when it waits for a free slot.- Not waited for
202withX-Run-Wait: overloadedandLocation: /api/runs/<id>, when this server already holds as many waiting callers as it can, or your organization’s share of them. The body hasrunId,statuspending,waitoverloadedandpoll. The run is queued and runs; it is never dropped. Read its result by its ID. A self-hosted installation sets the limits withSYNC_WAITS_MAX(4,096 per API process unless changed) andSYNC_WAITS_PER_ORG(an eighth of it).- The run failed
500, or504when a time limit stopped it, withrunIdandstatus. The reason isn’t sent: read the run.- Someone cancelled it
409withrunIdandstatus, while it waited for a slot or while it ran.X-Run-Statussayscancelled.
curl -i -X POST "$HOOK" \
-H "Content-Type: application/json" \
-d '{"name": "Ada Lovelace"}'HTTP/1.1 200 OK
Content-Type: application/json
X-Run-Id: f91487db-…
X-Run-Status: completed
{"result": "ADA LOVELACE"}4.Don’t wait
Add ?async=true. GoRunner answers 202 at once with the run’s ID, and runs the workflow after.
Follow it with Get a run, or follow it as it happens.
curl -X POST "$HOOK?async=true" \
-H "Content-Type: application/json" \
-d '{"orderId": "A-1002"}'{
"data": {
"message": "Workflow triggered",
"runId": "c73bfcdb-…"
}
}Retry safely
A sender that retries after a lost answer or a 202 starts a second run. To stop that, send an Idempotency-Key header with the call, the same on every retry. Without it, nothing changes.
- The key
- Up to 255 characters. Kept 24 hours, for this workflow’s live runs only.
- The same key, the same request
- No new run. You get the first run’s answer again, with
Idempotency-Replayed: true, or202with its ID while it still runs. - The same key, another request
422. The method, query and body must match.- While the first is recorded
409withRetry-After: 1.- A longer key
400.
A call is never refused for its key. Past 10,000 live keys for one workflow, or while Redis can’t be reached, it runs without the check. Apps’ own triggers, such as Slack or Stripe, don’t take one.
When it’s refused
Errors come in the API’s usual shape. Retry the ones that send Retry-After.
- 404
NOT_FOUND“Webhook not found.” The address is wrong, or the workflow isn’t published.
The trigger doesn’t allow this method.
Allowlists the ones it does.- 401
UNAUTHORIZEDThe trigger’s Authentication didn’t match.
The body is over 5 MiB.
The workflow is at its limit and set to refuse.
RUNS_WAITING_LIMIT: too many runs already wait.- 429
RATE_LIMITEDToo many calls from one address: 3,000 a minute unless a self-hosted installation changes
WEBHOOK_RATE_LIMIT. - 503
BUSYThe server is holding as much request data as it can. Nothing was done, so a retry is safe.
Retry-After: 2. GoRunner couldn’t look up the address just now. Nothing was done. Retry after
Retry-After.Nothing can run the workflow just now, and nothing was recorded.
Retry-After: 10. With?async=true, the run is queued instead.The run finished, but its answer couldn’t be read back. The run is intact: read it with the ID in
X-Run-Id.The account is suspended.
Retry-After: 3600.- 503
PAUSEDA self-hosted installation has no license.
Retry-After: 3600.
Why didn’t a HEAD or OPTIONS request start a run?
They never do. HEAD answers 200 and OPTIONS answers 204 with the allowed methods, so a service can check the address. Sign-up checks from Microsoft Graph, hub.challenge services and Asana are answered too, without a run.
What about a Form trigger’s address?
It is a page for a person, on the same address. GET shows the form. POST takes form fields, multipart or a JSON object, and answers with a page: 200, or 400 and 415 with the form again. Any other method is 405.
Can I call it before publishing?
Not as a live run: a draft’s address answers 404. In the builder, press Listen for the next real event in the trigger’s test data. The next call to the address within 120 seconds is captured and runs the draft as a test.
Carry on a waiting run
A Delay / Wait › Wait for Approval / Webhook step gives {{steps.<name>.resumeUrl}}, at /api/webhooks/resume/…. Send it to the system that answers.
POST- Carries on the run. The query and the JSON body reach the step as
resumeData.decision=approveorrejectanswers an approval. - The answer
200withrunId,statusresumingandwaitStep. A caller asking for HTML gets a page.- Once
- A link works once. After that it answers
404:ALREADY_ANSWEREDif it was answered,TIMED_OUTif the step’s wait ran out,NOT_FOUNDif it never existed. - Up to 5 MiB
- A longer body is
413, and the link still works. The same goes for503and500: nothing was decided, so send it again. While the account is suspended, it is409 ACCOUNT_SUSPENDED. - A browser
- Opening it shows a page with a button, so a link scanner can’t answer it. A
GETthat doesn’t ask for HTML answers it, as aPOSTdoes.
Related
- Start from a webhookThe Webhook trigger in the builder · 5 min
- Ask for approvalHold a run until a person answers · 4 min
- Get a runIts state and each step’s output
Checked against 12d85b3b