Event ingress
Turn signed HTTP callbacks into workflow events.
Webhook receivers verify incoming requests, run a small JavaScript handler, and publish normalized events that workflows subscribe to by source and type.
When to Use Webhooks
Use a webhook when another system should start work and can send a signed HTTP callback. Keep provider parsing in the receiver handler, emit a small normalized event, then let a workflow decide what happens next with scoped capabilities and approvals.
Mental Model
Webhooks are an ingress path, not workflow triggers by themselves. A receiver verifies the request, runs a short JavaScript handler in the safe Wasm JS runtime, and the handler may emit one or more workflow events. Workflows subscribe to those emitted events by source and eventType.
Set It Up Through an Agent
The Webhooks page opens a setup agent with webhook management context. For an end-to-end setup, give the session concrete webhook receiver and workflow grants, then ask it to fetch the focused guides before saving anything.
Create a custom webhook receiver for our incident router.
It should accept signed JSON payloads, emit incident.created events, simulate the handler with a sample payload, then create an enabled workflow that subscribes to that event and starts an investigation run.
Use source webhook:incident-router and event type incident.created.
Use the provider delivery id header x-incident-event-id as the idempotency key.
Verify x-incident-signature as HMAC-SHA-256 of the raw body with the receiver secret.The agent should call gateway:webhook_setup_guide, list or get existing receivers, create or update the receiver, simulate the handler, call gateway:workflow_guide, validate the workflow definition, and then save the workflow.
Custom Receiver Example
A custom receiver documents its expected event types for people and agents. The handler still owns the real event emission, so keep the script small and normalize provider payloads before workflows consume them.
async (input, webhook) => {
const expected = "sha256=" + webhook.crypto.hmacSha256Hex(input.receiver.secret, input.rawBodyText);
const actual = input.headers["x-incident-signature"] || "";
if (!webhook.crypto.timingSafeEqual(actual, expected)) {
return webhook.reject(401, "invalid signature");
}
if (input.bodyParseError) {
return webhook.reject(400, input.bodyParseError);
}
const body = input.body && typeof input.body === "object" ? input.body : {};
const incidentId = typeof body.id === "string" ? body.id : null;
const eventId = input.headers["x-incident-event-id"];
if (!incidentId) {
return webhook.reject(400, "missing incident id");
}
if (!eventId) {
return webhook.reject(400, "missing incident event id");
}
webhook.emit({
type: "incident.created",
idempotencyKey: eventId,
value: {
incidentId,
severity: body.severity ?? "unknown",
title: body.title ?? "Untitled incident",
receiverId: input.receiver.id
}
});
return webhook.accept();
}Workflow Subscription Example
The workflow receives the handler's value as ctx.input.event. It also receives trigger metadata under ctx.input.trigger.
{
"name": "Incident webhook investigation",
"enabled": true,
"triggers": [
{
"id": "incident-created",
"kind": "event",
"source": "webhook:incident-router",
"eventType": "incident.created",
"enabled": true
}
],
"definition": {
"runtime": "graph",
"startStepId": "main",
"steps": [
{
"id": "main",
"kind": "wasm-js",
"entrypoint": "main",
"source": "export function main(ctx) { return { ok: true, incidentId: ctx.input.event.incidentId, severity: ctx.input.event.severity }; }"
}
]
}
}GitHub Preset Example
Use the GitHub preset when the producer is GitHub. ExecWarden verifies x-hub-signature-256, readsx-github-event and payload action, then emits source: "github" events such as pull_request.opened, pull_request.synchronize, push, and workflow_job.queued.
{
"triggers": [
{
"id": "pr-opened",
"kind": "event",
"source": "github",
"eventType": "pull_request.opened",
"enabled": true
}
]
}