Webhook
Trigger workflow execution when an external system sends an HTTP request to a Scrydon-generated URL.
The Webhook block generates a unique HTTP endpoint. Any external service that can send an HTTP POST request can trigger your workflow.
How it works
- Add a Webhook block to your workflow.
- A unique webhook URL is generated for that block — copy it from the block's Trigger Link field.
- Configure your external service (GitHub, Stripe, any custom system) to POST to that URL.
- When the request arrives, the workflow starts and the request body, any files it carried, headers, method, and query parameters are injected as block outputs.
The editor registers the webhook after its pending changes finish saving. Until the server confirms registration, the Trigger Link remains empty instead of showing a placeholder URL. If registration fails, the block displays an error with a retry action. Trigger-managed webhooks are part of the block lifecycle; remove the trigger block instead of deleting its webhook separately.
Testing in a writable environment
You don't need to deploy or promote to test a webhook. In any writable environment, calling the webhook URL runs the live/draft workflow — exactly what's on the canvas — so you can iterate and test end to end as you build.
In any read-only environment, the webhook runs the deployed snapshot instead and requires an active deployment version. Without one the call returns 403 with the code WORKFLOW_NOT_DEPLOYED; the Monitor page marks such a workflow Not deployed — triggers blocked and offers a Deploy action that creates the snapshot without changing the webhook URL. Environment names such as Development, Testing, Staging, and Production are conventions chosen by your organization, not fixed platform roles. Each environment has its own webhook URL, and promoting the workflow again into the same environment keeps that URL — you configure the external system once per environment. A webhook-triggered run acts as the environment's Run As identity (the workflow owner when none is set), exactly like a scheduled or API-triggered run; see Run As. See Triggers → Environments and testing for the full model.
Calling from Monitor
In Monitor, expand a workflow — in the Table view or the Timeline view, both list the same triggers — to see each webhook trigger's URL, HTTP method, authentication mode and credential carrier. Copy URL copies the bare URL and Copy example request copies a curl example that keeps placeholders such as $WEBHOOK_TOKEN or $WEBHOOK_SECRET, so the credential is never rendered on the page. In an expression such as '…?token='"$WEBHOOK_TOKEN", the adjacent quoted fragments are shell syntax that form one URL argument; the quote characters are not part of the URL.
This surface generates a test URL and runnable curl; it does not send a second HTTP request or
render a separate in-editor result. Run the command in your HTTP client to see the canonical
response body. The resulting execution is persisted and can be opened in Monitor by its
executionId, including after a reload.
When the webhook requires a configured credential, use Copy webhook token or Copy HMAC secret to copy the effective deployed value. Use Copy runnable cURL to copy a complete command with that same value already inserted. For URL-token authentication, Copy authenticated URL copies the complete URL with the token inserted for systems that accept only a URL. Scrydon fetches the credential only after you click one of these actions. It never renders or caches the value in Monitor. A workspace-secret reference is resolved in the workspace currently shown in Monitor.
Anyone who can view that workflow in Monitor can use these copy actions. To change or rotate the credential, update the source Webhook block or its referenced workspace secret.
A copied token, HMAC secret, authenticated URL, or runnable command is
sensitive clipboard data. An authenticated URL can also be recorded in
browser history, proxy logs, and Referer headers.
Paste it only into a trusted destination and clear your clipboard when you no
longer need it.
Who called your webhook
For every call to a webhook URL — accepted, duplicate, or refused — Scrydon records who called it: the caller's address as seen by the platform edge, the client's User-Agent string, whether authentication passed, and the HTTP status Scrydon answered with. This includes calls that never started a run, such as a failed authentication check or an address not on the allowlist. While a webhook is over its rate limit, or the rate limit cannot be checked, one refused call per minute is recorded rather than every one, so a flood of calls does not flood the history.
The recorded address is the one the platform edge saw the request arrive from. If the call passed through another proxy or load balancer upstream of Scrydon, that hop's address is what's recorded — not necessarily the originating client behind it.
The caller shows up in three places:
- Run detail — a run started by a webhook opens with a Caller section (From, Client, Auth, Event ID) above the output. The run sheet's top bar names only the trigger; the caller's address lives in that section's From field.
- Monitor — each webhook trigger's row shows its most recent call (
Last call <time> from <address>) and how many calls were rejected in the last 24 hours, with a link to that trigger's full call history. - Deliveries — opens the full history for one webhook trigger: calls grouped by caller address and client, then every individual call newest first, filterable to All or Rejected, over the last 24 hours, 7 days, or 30 days. Selecting a call opens it in the run sheet — a call that started a run opens that run.
Caller records are kept for 30 days, the same retention as the rest of a workflow's trigger history. They are visible to whoever can view that workflow in Monitor. The logs API (GET /api/v1/logs/{id}) does not return them: an API key or service account reads a call's event without the caller's address and client.
Refused calls do not appear on the Monitor timeline, which shows runs and accepted calls. A burst of rejected callers therefore does not mark a healthy workflow as failing. Refused calls stay in the trigger's Deliveries history.
The caller's address and User-Agent also arrive as ordinary request headers
(X-Forwarded-For, X-Real-IP, User-Agent), and the webhook block passes
its request headers to the run — only credential headers and cookies are
removed. Downstream blocks can read them through <webhook1.headers>, and
they are stored with the run's log for as long as run logs are kept, visible
wherever that run's log is.
Available variables
Reference the incoming request in downstream blocks using the block's name as the prefix:
| Variable | Description |
|---|---|
<webhook1.body> | The request body, decoded by its Content-Type — see Request bodies and files |
<webhook1.files> | Files the request carried, as a list of files (empty when there were none) |
<webhook1.headers> | Request headers as a JSON object |
<webhook1.method> | HTTP method (POST) |
<webhook1.query> | Query string parameters as a JSON object |
<webhook1.remoteAddress> | The caller's address, as the platform edge saw it |
<webhook1.receivedAt> | When the request arrived, as an ISO-8601 timestamp |
Replace webhook1 with whatever name you gave the block.
Request bodies and files
A webhook accepts JSON, form posts, plain text, and files. The Content-Type header decides what the workflow sees:
| Request | <webhook1.body> | <webhook1.files> |
|---|---|---|
JSON (application/json, application/*+json) | The parsed JSON | Empty |
Form post (application/x-www-form-urlencoded) | The form fields as a JSON object | Empty |
Multipart form (multipart/form-data) | The text fields as a JSON object | One entry per file part |
Text (text/*, XML, or no Content-Type) | The raw text as a string | Empty |
Any other type (audio/*, image/*, application/pdf, …) | {} | The body, as one file |
A body that parses as JSON is always treated as JSON, whatever its declared type. A body declared as application/json that does not parse returns 400 Invalid payload format and does not start the workflow.
Each file in <webhook1.files> has the same shape as the files a Start block receives: name, type, size, a storage key, and a download url. Pass <webhook1.files> to a block that takes a list of files, or <webhook1.files[0]> to a block that takes one file, such as a speech-to-text block. The file's bytes are stored with the run and are not copied into <webhook1.body>. The download url expires 10 minutes after the request arrives.
# A multipart upload: the text fields land in <webhook1.body>, the file in <webhook1.files>
curl -X POST "https://app.scrydon.com/agentic/api/webhooks/trigger/{path}" \
-H "Authorization: Bearer $WEBHOOK_TOKEN" \
-F "unit=7" \
-F "audio=@call-42.wav;type=audio/wav"
# A raw file body: named from Content-Disposition, or upload.<extension> without one
curl -X POST "https://app.scrydon.com/agentic/api/webhooks/trigger/{path}" \
-H "Authorization: Bearer $WEBHOOK_TOKEN" \
-H "Content-Type: audio/mpeg" \
-H 'Content-Disposition: attachment; filename="call-42.mp3"' \
--data-binary @call-42.mp3The webhook's request size limit, 10 MB unless the block sets another, applies to the whole request, files included. For a larger file, such as a long recording, use an upload session. A multipart request can carry at most 20 files; more return 400. File names keep only their last path segment, so ../../clip.wav is stored as clip.wav. Two files with the same name in one request are stored as clip.wav and clip-2.wav.
The text fields of a multipart form are also available directly as <webhook1.fieldName>, like the top-level keys of a JSON body. <webhook1.files> always holds the request's files: a JSON body with its own files key is read as <webhook1.body.files>.
Request size
The Webhook block's Max Request Size (MB) sets the largest request the webhook accepts, files included: 1, 5, 10, 25, 50 or 100 MB. It defaults to 10 MB. A megabyte here is 1,048,576 bytes, so 10 MB is 10,485,760 bytes.
- A block in None authentication mode accepts at most 10 MB, whatever the setting. Scrydon reads a request's body before it checks the credential, because an HMAC signature is computed over the body. A URL anyone can call therefore never makes Scrydon hold more than 10 MB.
- Vendor-specific triggers accept 10 MB.
- The platform edge allows 60 seconds for a whole request, so 100 MB needs an uplink of about 14 Mbit/s. On a slower link, or for a file over 100 MB, use an upload session.
A larger request answers 413 Payload Too Large, naming the limit (Payload too large: this webhook accepts requests up to 25 MB), and does not start the workflow. The refusal is recorded in the trigger's Deliveries history. A request to a URL that matches no webhook answers 404 without its body being read. Like the authentication fields, the limit is read from the block when each request arrives.
Large files: upload sessions
A file larger than a single request can carry, up to 5 GiB, is sent in parts through an upload session. The run sees it in <webhook1.files> exactly like a file sent in one request.
Upload sessions are available on the generic Webhook block, and only when the block has authentication configured: a block in None mode answers 403. The organization's storage must be an object store (S3 or Azure Blob); local storage answers 501.
| Step | Request | Credential |
|---|---|---|
| 1. Open | POST /api/webhooks/trigger/{path}/uploads | The webhook's own authentication |
| 2. Send each part | PUT /api/webhooks/uploads/parts/{partNumber} | Authorization: Bearer <upload token> |
| 3. Complete | POST /api/webhooks/uploads/complete | Authorization: Bearer <upload token> |
| Abort (optional) | POST /api/webhooks/uploads/abort | Authorization: Bearer <upload token> |
1. Open the session. Send the file's name, media type and exact size in bytes, authenticated exactly as a normal delivery to the webhook would be (for an HMAC webhook, sign this JSON body). type is optional and defaults to application/octet-stream.
{ "name": "call-42.wav", "type": "audio/wav", "size": 734003200 }A 201 Created response carries the session:
{
"executionId": "5b0f…",
"token": "eyJhbGciOiJIUzI1NiJ9…",
"expiresAt": "2026-10-10T09:00:00.000Z",
"partSize": 8388608,
"partCount": 88,
"urls": {
"part": "https://app.scrydon.com/agentic/api/webhooks/uploads/parts/{partNumber}",
"complete": "https://app.scrydon.com/agentic/api/webhooks/uploads/complete",
"abort": "https://app.scrydon.com/agentic/api/webhooks/uploads/abort"
}
}2. Send each part as the raw request body, numbered 1 to partCount. Every part is exactly partSize bytes (8 MiB) except the last, which holds the rest of the file; a part of any other length answers 400 or 413. Each part answers { "partNumber": 3, "tag": "…" }; keep every tag. Parts can be sent in any order and in parallel. Re-sending a part replaces it, so use the tag from its latest send. A 502 means the part was not stored: send it again.
3. Complete the session with every part's tag and, optionally, the JSON the workflow reads as <webhook1.body>:
{
"parts": [
{ "partNumber": 1, "tag": "…" },
{ "partNumber": 2, "tag": "…" }
],
"body": { "unit": 7 }
}parts must list every part from 1 to partCount, each exactly once. Completing answers exactly as a single-request delivery does: 200 {"message":"Webhook processed"}, or the workflow's result when Wait for Completion is on. Completing the same session again never starts a second run; it answers {"message":"Webhook already processed","duplicate":true}. So a complete that timed out or returned a 5xx is safe to send again.
If the block's event-type filter skips the completed request, the parts are discarded and no run starts.
URL="https://app.scrydon.com/agentic/api/webhooks/trigger/{path}"
FILE=call-42.wav
SIZE=$(wc -c < "$FILE" | tr -d ' ')
session=$(curl -sf -X POST "$URL/uploads" \
-H "Authorization: Bearer $WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$FILE\",\"type\":\"audio/wav\",\"size\":$SIZE}")
UPLOAD_TOKEN=$(jq -r .token <<<"$session")
PART_SIZE=$(jq -r .partSize <<<"$session")
PART_URL=$(jq -r .urls.part <<<"$session")
parts="[]"
for n in $(seq 1 "$(jq -r .partCount <<<"$session")"); do
tag=$(dd if="$FILE" bs="$PART_SIZE" skip=$((n - 1)) count=1 2>/dev/null \
| curl -sf -X PUT "${PART_URL%"{partNumber}"}$n" \
-H "Authorization: Bearer $UPLOAD_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @- \
| jq -r .tag)
parts=$(jq -c --argjson n "$n" --arg tag "$tag" '. + [{partNumber: $n, tag: $tag}]' <<<"$parts")
done
curl -sf -X POST "$(jq -r .urls.complete <<<"$session")" \
-H "Authorization: Bearer $UPLOAD_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"parts\":$parts,\"body\":{\"unit\":7}}"The upload token is a credential for this one session: anyone holding it can
send parts and complete the run. It expires at expiresAt, 24 hours after the
session opens, and cannot be revoked earlier. Abort a session you no longer
need. Scrydon redacts the token from <webhook1.headers>.
Only opening a session counts against the webhook's rate limit. On the workflow's monitor, opening a session is recorded only when it is refused. Completing it is recorded like a delivery: the one that started the run, a duplicate, or a refusal, shown with Bearer authentication because the upload token is what the completing request presents. A request whose upload token is missing, invalid or expired cannot be tied to a webhook and is not recorded. Neither are individual parts or an abort.
Authentication
Generic webhook endpoints accept only POST. In the block's Authentication field, choose one of these modes:
New Webhook blocks start in Bearer Token mode and generate a token automatically. Request bodies are limited to 10 MB by default; see Request size.
Each webhook URL has its own token bucket: it allows a burst of 20 requests and replenishes 10 tokens every 60 seconds. When the bucket is exhausted, Scrydon returns 429 Too Many Requests with a Retry-After header and does not authenticate or start the workflow. If rate-limit enforcement is temporarily unavailable, Scrydon fails closed with 503 Service Unavailable and Retry-After: 5. Producers should wait for the supplied interval before retrying either response.
| Mode | Block configuration | Incoming request |
|---|---|---|
| Bearer Token | Bearer Token generated when the block is created | Send Authorization: Bearer <token>. This is the default and recommended mode. |
| Static Header | Header Name and Header Value | Send the configured header with the exact configured value. Header names are case-insensitive; values are case-sensitive. |
| HMAC Signature | HMAC Header, HMAC Secret, and HMAC Algorithm | Send an HMAC of the exact request-body bytes in the configured header. |
| URL Token | Token Parameter (defaults to token) and Token Value | Send the token as the configured query parameter. |
| None | No additional fields | No authentication credential is required. |
URL Token is the weakest mode — reach for it last. A credential in a URL
reaches reverse-proxy access logs, browser history, and Referer headers,
none of which Scrydon controls. It exists for producers that can only be
handed a URL and cannot set a header or sign a body; for that class the only
alternative is None, i.e. no authentication at all. If your producer can
send headers, use Bearer Token, HMAC Signature, or Static Header
instead.
Scrydon redacts the configured token parameter from <webhook1.query> and
from run logs. It cannot redact an upstream proxy's access log.
URL token
# As a query parameter — for producers that only accept a URL
curl -X POST "https://app.scrydon.com/agentic/api/webhooks/trigger/{path}?token=$WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event": "order.created", "orderId": "42"}'Use a workspace-secret reference such as {{GENERIC_WEBHOOK_TOKEN}} for Token
Value, exactly as with the other modes.
A malformed Token Parameter is rejected as invalid configuration rather than silently falling back to the default — a parameter name that could never match the incoming request would mean the credential is never checked while the request still reports as authenticated.
Before exposing <webhook1.headers> to the workflow, Scrydon redacts the configured authentication header and standard credential-bearing headers such as Authorization, Proxy-Authorization, Cookie, and Set-Cookie.
Use a Scrydon workspace-secret reference such as {{GENERIC_WEBHOOK_TOKEN}} for Bearer Token, Header Value, or Token Value, or {{GENERIC_WEBHOOK_HMAC_SECRET}} for HMAC Secret. Scrydon resolves the reference when authenticating the request. A missing or unavailable reference is rejected.
The password-style fields only mask their contents in the editor. A literal value is stored as plaintext in the workflow JSON. Use a workspace-secret reference so the credential remains behind the Scrydon secret vault.
Bearer token
The generated Bearer token can be copied from Monitor after the workflow is deployed:
curl -X POST "https://app.scrydon.com/agentic/api/webhooks/trigger/{path}" \
-H "Authorization: Bearer $WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event": "order.created", "orderId": "42"}'Static header
For example, configure Header Name as X-Webhook-Token and Header Value as {{GENERIC_WEBHOOK_TOKEN}}, then send the resolved value:
curl -X POST "https://app.scrydon.com/agentic/api/webhooks/trigger/{path}" \
-H "Content-Type: application/json" \
-H "X-Webhook-Token: $WEBHOOK_TOKEN" \
-d '{"event": "order.created", "orderId": "42"}'HMAC signature
Scrydon computes the HMAC over the exact bytes received, before JSON parsing, text decoding, normalization, or reserialization. Whitespace, property order, Unicode encoding, and a trailing newline therefore change the signature.
The supported algorithms are sha1, sha256, and sha512. Use sha256 unless your caller requires another supported algorithm. The signature header accepts either a bare hexadecimal digest or an algorithm-prefixed digest:
<hex-digest>
sha256=<hex-digest>When a prefix is present, it must match the configured algorithm. Hexadecimal digits are case-insensitive. This example signs and sends exactly the bytes held in body:
body='{"event":"order.created","orderId":"42"}'
signature="$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | awk '{print $NF}')"
curl -X POST "https://app.scrydon.com/agentic/api/webhooks/trigger/{path}" \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=$signature" \
--data-binary "$body"Every authentication failure—missing or wrong headers, malformed signatures, invalid configuration, or unavailable secrets—returns the same response and does not start the workflow:
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Cache-Control: no-store
UnauthorizedHMAC authenticates the body and possession of the shared secret, but it does not prevent replay. A caller can resend a previously valid body and signature because this mode has no timestamp or nonce.
Duplicate deliveries
Webhook delivery is at-least-once, so Scrydon deduplicates repeated deliveries of the same event. It identifies an event from the first of these it finds:
- a delivery-id header —
webhook-id,x-webhook-id,x-event-id,x-github-delivery,x-shopify-webhook-id,x-teams-notification-id - an id field in the JSON body —
eventId,event_id,messageId,notificationId,deliveryId, orid(also one level down underdata) - a hash of the request body, when the request carries none of the above
A repeat of an event Scrydon has already accepted returns 200 OK with:
{ "message": "Webhook already processed", "duplicate": true }The workflow is not started a second time, and the delivery is recorded on the
workflow's monitor as push_duplicate.
This holds whichever Scrydon server receives the repeat, and across restarts:
- A run that is still in progress is never started again, however long it runs.
- A finished run is not started again for 24 hours after its delivery was accepted. A repeat that arrives later starts a new run.
- A delivery whose run could not be started can be sent again: its repeat starts the run.
Step 3 is why re-sending an identical body does not start a second run: with no delivery id anywhere in the request, the body is the only thing that can distinguish a provider's retry from a genuinely new event, so Scrydon treats identical bytes as the same event.
If you are testing and want each send to start its own run, vary the delivery
id — add an x-event-id header, or change the id / eventId field in the
body. Changing an unrelated part of the body is not enough when the body
already carries an id field, because step 2 wins over step 3.
Vendor-specific triggers
For services with a dedicated Scrydon integration (GitHub, Microsoft Graph, Atlassian, etc.), prefer the vendor's trigger block over the generic Webhook block. Vendor triggers parse and validate the event payload automatically and surface typed variables.
See Vendors for the full list of integrations with trigger support.
Responding with the workflow result
By default a webhook acknowledges immediately with 200 {"message":"Webhook processed"} and the workflow runs in the background. Enable Wait for Completion on the block to respond with the result instead:
{ "executionId": "…", "status": "completed", "outputs": { "response": { … } } }A Response block's output appears under outputs. If the run exceeds 30 seconds the request returns 202 with { "executionId", "status": "running" } — the execution continues, and the id lets you follow it.
A workflow that reaches a terminal failure still returns HTTP 200, because the webhook request
was accepted and the workflow finished. Its response includes the client-safe reason:
{
"executionId": "…",
"status": "failed",
"error": {
"code": "INTEGRATION_ACCOUNT_NOT_WORKSPACE_ENABLED",
"message": "Service account sa_… has no Microsoft Teams (org:scrydon:microsoft:teams) account it can use in this workspace. Link one to it under Settings → Members → Service Accounts → Credentials, or assign an execution credential to the workspace under Settings → Workspaces → Execution credentials.",
"retryable": false,
"blockId": "teams-1"
}
}Authentication, malformed input, and queue/infrastructure rejection keep their normal 4xx/5xx
status. A 202 running response is only the synchronous poll timeout; query the execution status
later or open the run in Monitor to see the persisted terminal error. Monitor shows the same code
and message after reload and focuses the failing block when blockId is present.
When a block fails because the account it acts through cannot serve the call,
error.code may be one of these codes. Each one names what to fix:
| Code | What to fix |
|---|---|
INTEGRATION_ACCOUNT_NOT_CONNECTED | The connection has no account it can use. Connect one under Settings → Account → Connected Accounts, or assign one to the workspace. |
INTEGRATION_ACCOUNT_REAUTHORIZATION_REQUIRED | The account's sign-in expired or was revoked. Reconnect the account. |
INTEGRATION_ACCOUNT_MISSING_SCOPES | The account's grant lacks permissions the extension needs. Reconnect it and accept every permission it asks for. |
INTEGRATION_ACCOUNT_NOT_WORKSPACE_ENABLED | No account the run may use is assigned to this workspace. Assign one under Settings → Workspaces → Execution credentials. |
Leave this off unless the caller expects it. Webhook producers are usually machines with short client timeouts — Keel's is 5 seconds, and it treats a slow reply as a delivery failure and retries, so a synchronous response can turn one event into a retry storm. The immediate acknowledgement exists for that reason.