Webhooks Plugin: Authenticated TaskFlow Ingress for External Automation
Learn how to configure the Webhooks plugin to expose authenticated HTTP endpoints for trusted external systems like Zapier, n8n, or CI jobs to create and manage TaskFlows. This guide covers route setup and Gateway requirements.
Read this when
- You want to create or update TaskFlow records from an external system
- You are configuring the bundled webhooks plugin
The Webhooks plugin exposes authenticated HTTP endpoints, letting a trusted outside system, such as Zapier, n8n, a CI job, or an internal service, create and operate managed OpenClaw TaskFlows through HTTP. This works without the need for a custom plugin.
This plugin operates within the Gateway process. When the Gateway is remote, you must install and configure the plugin on that host, then restart the Gateway. By default, no routes are set up, so the plugin does nothing until you define at least one route.
Configure routes
Configuration goes under plugins.entries.webhooks.config:
{
plugins: {
entries: {
webhooks: {
enabled: true,
config: {
routes: {
zapier: {
path: "/plugins/webhooks/zapier",
sessionKey: "agent:main:main",
secret: {
source: "env",
provider: "default",
id: "OPENCLAW_WEBHOOK_SECRET",
},
controllerId: "webhooks/zapier",
description: "Zapier TaskFlow bridge",
},
},
},
},
},
},
}
Fields for each route:
| Field | Required | Default | Notes |
|---|---|---|---|
enabled | no | true | |
path | no | /plugins/webhooks/<routeId> | Must be unique across routes. |
sessionKey | yes | - | Session that owns the bound TaskFlows. |
secret | yes | - | Plain string or a SecretRef (below). |
controllerId | no | webhooks/<routeId> | Used as the default create_flow controller. |
description | no | - | Operator note only. |
secret can hold either a plain string or a SecretRef: { source: "env" | "file" | "exec" | "store", provider: "default", id: "..." }.
SecretRefs are resolved against the Gateway's startup config snapshot. If one route's secret fails to resolve, the Gateway continues running, and that route stays registered but inactive: requests get a generic authentication failure (401). All other routes keep working. Correct the SecretRef source, then reload or restart the Gateway to pick up the new snapshot. SecretRef values are never resolved on the public request path.
Security model
Every route operates with the TaskFlow authority of its configured sessionKey: it can inspect and modify any TaskFlow owned by that session. TaskFlow access always goes through api.runtime.tasks.managedFlows.bindSession(...), so a route can never act outside its bound session. To reduce blast radius:
- Use a strong, unique secret per route.
- Prefer a SecretRef over an inline plaintext secret.
- Bind routes to the narrowest session that fits the workflow.
- Expose only the specific webhook path you need.
For each path, requests are handled in this order: HTTP method (POST only) and Content-Type: application/json checks, then fixed-window rate limiting (120 requests per 60-second window per path+client-IP key, up to 4,096 tracked keys), then in-flight request limiting (8 concurrent requests per key, up to 4,096 tracked keys), then shared-secret authentication, then a 256 KB / 15-second JSON body read. Requests failing an earlier check never reach later ones.
Request format
Send POST requests with Content-Type: application/json and either Authorization: Bearer <secret> or x-openclaw-webhook-secret: <secret>:
curl -X POST https://gateway.example.com/plugins/webhooks/zapier \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_SHARED_SECRET' \
-d '{"action":"create_flow","goal":"Review inbound queue"}'
Supported actions
| Action | Purpose |
|---|---|
create_flow | Create a managed TaskFlow for the route's session. |
get_flow | Fetch one TaskFlow by id. |
list_flows | List TaskFlows for the route's session. |
find_latest_flow | Fetch the most recently updated TaskFlow. |
resolve_flow | Resolve a TaskFlow by opaque token. |
get_task_summary | Fetch the task summary for a TaskFlow. |
set_waiting | Mark a TaskFlow waiting, with optional state/wait data. |
resume_flow | Resume a waiting/blocked TaskFlow. |
finish_flow | Mark a TaskFlow finished. |
fail_flow | Mark a TaskFlow failed. |
request_cancel | Request cooperative cancellation. |
cancel_flow | Cancel a TaskFlow (may return 202 if children are still active). |
run_task | Create a managed child task inside an existing TaskFlow. |
Mutating actions (set_waiting, resume_flow, finish_flow, fail_flow, request_cancel) require flowId and expectedRevision for optimistic concurrency; a stale revision returns 409 revision_conflict.
create_flow
{
"action": "create_flow",
"goal": "Review inbound queue",
"status": "queued",
"notifyPolicy": "done_only"
}
run_task
Allowed runtime values: subagent, acp. startedAt, lastEventAt, and
progressSummary are only valid when status is "running"; sending them
with any other status returns 400 invalid_request.
{
"action": "run_task",
"flowId": "flow_123",
"runtime": "acp",
"childSessionKey": "agent:main:acp:worker",
"task": "Inspect the next message batch"
}
Response shape
{
"ok": true,
"routeId": "zapier",
"result": {}
}
{
"ok": false,
"routeId": "zapier",
"code": "not_found",
"error": "TaskFlow not found.",
"result": {}
}
Flow and task views never include owner/session metadata, so responses cannot
leak the route's bound sessionKey. code values include not_found,
not_managed, revision_conflict, persist_failed, cancel_requested,
cancel_pending, terminal, invalid_request, request_rejected, and
action-specific fallback codes (mutation_rejected, create_rejected,
task_not_created, cancel_rejected) when a mutation is rejected for a
reason not covered by the named codes above.
Related
- Hooks - internal event-driven hooks vs. this HTTP-based TaskFlow bridge
- Gateway webhooks (
hooks.*config) - separate generic Gateway HTTP endpoint feature; not the same as this plugin's routes - Plugin runtime SDK
- CLI webhooks