GoRunner docs
Search or askSearch⌘K

Call a workflow’s webhook

Start a published workflow from any service with one HTTP call, and get its answer in the same call.

5 min · Needs curl and jq

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.

bash
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-key and the trigger’s own header are kept as [redacted].
{{trigger.method}}
POST, GET and so on.

A body may be up to 5 MiB.

bash
curl -i -X POST "$HOOK?source=crm" \
  -H "Content-Type: application/json" \
  -d '{"orderId": "A-1001"}'
Output
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 as Set-Cookie, fails the step, and you get 500. Check a definition warns of it.
Without one
200 and the last step’s output, with the run’s ID in X-Run-Id and its state in X-Run-Status. {} if no step gave output.
Still running
202 with runId, status and 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
202 with X-Run-Wait: overloaded and Location: /api/runs/<id>, when this server already holds as many waiting callers as it can, or your organization’s share of them. The body has runId, status pending, wait overloaded and poll. The run is queued and runs; it is never dropped. Read its result by its ID. A self-hosted installation sets the limits with SYNC_WAITS_MAX (4,096 per API process unless changed) and SYNC_WAITS_PER_ORG (an eighth of it).
The run failed
500, or 504 when a time limit stopped it, with runId and status. The reason isn’t sent: read the run.
Someone cancelled it
409 with runId and status, while it waited for a slot or while it ran. X-Run-Status says cancelled.
bash
curl -i -X POST "$HOOK" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ada Lovelace"}'
Output
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.

bash
curl -X POST "$HOOK?async=true" \
  -H "Content-Type: application/json" \
  -d '{"orderId": "A-1002"}'
Output
{
  "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, or 202 with its ID while it still runs.
The same key, another request
422. The method, query and body must match.
While the first is recorded
409 with Retry-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.

  • “Webhook not found.” The address is wrong, or the workflow isn’t published.

  • The trigger doesn’t allow this method. Allow lists the ones it does.

  • The 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.

  • Too many calls from one address: 3,000 a minute unless a self-hosted installation changes WEBHOOK_RATE_LIMIT.

  • 503BUSY

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

  • A 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=approve or reject answers an approval.
The answer
200 with runId, status resuming and waitStep. A caller asking for HTML gets a page.
Once
A link works once. After that it answers 404: ALREADY_ANSWERED if it was answered, TIMED_OUT if the step’s wait ran out, NOT_FOUND if it never existed.
Up to 5 MiB
A longer body is 413, and the link still works. The same goes for 503 and 500: nothing was decided, so send it again. While the account is suspended, it is 409 ACCOUNT_SUSPENDED.
A browser
Opening it shows a page with a button, so a link scanner can’t answer it. A GET that doesn’t ask for HTML answers it, as a POST does.

Checked against 12d85b3b