Files
openclaw/docs/plugins/webhooks.md
2026-08-26 23:12:06 -07:00

14 KiB

doc-schema-version, summary, read_when, title
doc-schema-version summary read_when title
1 Webhooks plugin: authenticated TaskFlow ingress for trusted external automation
You want to create or update TaskFlow records from an external system
You are configuring the bundled webhooks plugin
Webhooks plugin

The Webhooks plugin adds authenticated HTTP routes so a trusted external system (Zapier, n8n, a CI job, an internal service) can create and drive managed OpenClaw TaskFlow records over HTTP, without writing a custom plugin. create_flow creates a tracking record; run_task creates or links a child task record. Neither operation starts an agent. The external controller owns the workflow and advances its state.

To submit an agent turn from an external event, use Gateway HTTP hooks. To react to internal agent events, use internal hooks. Those surfaces do not share this plugin's routes or authentication.

The plugin runs inside the Gateway process. For a remote Gateway, install and configure it on that host, then restart the Gateway. It ships with no routes configured, so it is a no-op until you add at least one route.

Configure routes

Set config under plugins.entries.webhooks.config:

{
  plugins: {
    entries: {
      webhooks: {
        enabled: true,
        config: {
          routes: {
            zapier: {
              path: "/plugins/webhooks/zapier",
              sessionKey: "agent:main:hook:automation",
              secret: {
                source: "env",
                provider: "default",
                id: "OPENCLAW_WEBHOOK_SECRET",
              },
              controllerId: "webhooks/zapier",
              description: "Zapier TaskFlow bridge",
            },
          },
        },
      },
    },
  },
}

Route fields:

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 accepts a plain string or a SecretRef: { source: "env" | "file" | "exec" | "store", provider: "default", id: "..." }.

SecretRefs resolve into the Gateway's startup config snapshot. When one route's secret cannot resolve, the Gateway keeps running and that exact route stays registered but cold: requests receive a generic authentication failure (401). Other routes remain available. Fix the SecretRef source, then reload or restart the Gateway to activate the new snapshot. SecretRef values are never resolved on the public request path.

Security model

Each route authenticates with its own secret, not hooks.token or the Gateway auth token. Use HTTPS outside loopback. The endpoint accepts the action schema below, not arbitrary provider webhook payloads, URL verification challenges, or provider-specific HMAC signatures. Use an existing automation service to validate and translate external events when needed. Treat event content as data.

Each route acts with the TaskFlow authority of its configured sessionKey: it can inspect and mutate 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 limit 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.

Request handling order is: POST method, fixed-window rate limit, JSON content type, in-flight limit, shared-secret authentication, bounded JSON body read, then action validation. Earlier failures do not reach later checks.

Limit Scope
120 requests per 60 seconds Path plus client IP resolved using trusted proxy settings; at most 4,096 tracked keys.
8 concurrent requests Path plus socket peer address; at most 4,096 tracked keys. Clients behind one proxy can share this bucket.
256 KiB body, 15-second read Per request. This is a body-read timeout, not an agent-run timeout.

Request format

Send POST requests with Content-Type: application/json and either Authorization: Bearer <secret> or x-openclaw-webhook-secret: <secret>:

curl --include https://gateway.example.com/plugins/webhooks/zapier \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <route-secret>' \
  -d '{"action":"create_flow","goal":"Review inbound queue"}'

The successful response contains result.created: true and result.flow with its flowId, revision (initially 0), and status (default queued). Keep the flow id and read it back using the same route:

curl --include https://gateway.example.com/plugins/webhooks/zapier \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <route-secret>' \
  --data '{"action":"get_flow","flowId":"<returned-flow-id>"}'

HTTP 200 confirms a read or record operation, not completed agent work or message delivery. Use get_task_summary for linked task counts and task inspection for actual task status and delivery status. Keep those outcomes separate from the controller's flow status.

Supported actions

Action Purpose
create_flow Create a managed TaskFlow record for the route's session.
get_flow Fetch one TaskFlow by id.
list_flows List the session's TaskFlows, newest created first.
find_latest_flow Fetch the most recently created TaskFlow.
resolve_flow Resolve by flow id or the exact bound session key (its latest flow).
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 Record a cooperative cancellation request; does not cancel children itself.
cancel_flow Cancel a TaskFlow (may return 202 if children are still active).
run_task Create or link a managed child task record; does not start a run.

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. Read result.current or call get_flow, reconcile the change, then use its current revision. cancel_flow and run_task do not take expectedRevision. Action schemas reject unknown fields.

Read actions can return HTTP 200 with flow: null or summary: null when the flow is absent or outside the route's session. A successful read does not imply that a matching flow exists.

create_flow

{
  "action": "create_flow",
  "goal": "Review inbound queue",
  "status": "queued",
  "notifyPolicy": "done_only"
}

goal is required. Optional fields are controllerId, status (queued, running, waiting, blocked), notifyPolicy (done_only, state_changes, silent), currentStep, stateJson, and waitJson. The request's controllerId overrides the route default; it is not a separate authorization boundary.

Creation has no general idempotency key: retrying create_flow after an uncertain connection result can create a second flow. Reconcile with list_flows before repeating creation. Child success alone does not mark a managed flow finished; the controller must advance or finish the flow when appropriate.

run_task

Required fields are flowId, runtime, and task. Allowed runtime values are subagent and acp; status defaults to queued and can be running. startedAt, lastEventAt, and progressSummary are only valid when status is "running"; sending them with any other status returns 400 invalid_request.

The following links an already existing, currently owned backing run. Use the managed flow id returned by create_flow, and the childSessionKey and runId from that existing run. Inventing a child key or run id does not start work or grant authority.

{
  "action": "run_task",
  "flowId": "<flow-id>",
  "runtime": "acp",
  "childSessionKey": "<existing-child-session-key>",
  "runId": "<existing-run-id>",
  "task": "Inspect the next message batch"
}

childSessionKey requires the exact runId and current backing ownership by the route's configured session. Foreign, stale, or replaced runs are rejected at use time. Omit childSessionKey to create an unbacked tracking record; supplying an invalid backing reference is rejected. Reuse by runId is scoped to the runtime, owner, child, and flow; it is not a universal request-replay guarantee.

Optional metadata includes sourceId, parentTaskId, agentId, label, preferMetadata, and notifyPolicy (done_only, state_changes, silent). startedAt and lastEventAt are nonnegative integer timestamps in milliseconds.

Waiting and completion

set_waiting accepts currentStep, stateJson, waitJson, blockedTaskId, and blockedSummary. A nonempty blocked field selects blocked; otherwise the flow becomes waiting. resume_flow accepts status (queued default or running), currentStep, and stateJson, and clears waiting/blocked state.

finish_flow marks the flow succeeded and accepts stateJson; fail_flow marks it failed and also accepts blockedTaskId and blockedSummary. Optional string fields accept null to clear them; stateJson and waitJson accept any JSON value, including retained JSON null. Use the current expectedRevision for each transition.

Cancellation

request_cancel records cancellation intent. cancel_flow attempts cancellation of linked work. If children remain active, the response is HTTP 202 with ok: true, code: "cancel_pending", and result.cancelled: false. Check get_flow and get_task_summary afterward; 202 does not mean cancellation finished.

Response shape

{
  "ok": true,
  "routeId": "zapier",
  "result": {}
}
{
  "ok": false,
  "routeId": "zapier",
  "code": "not_found",
  "error": "TaskFlow not found.",
  "result": {}
}

Flow and task views omit owner/requester metadata such as ownerKey, requesterSessionKey, and requesterOrigin. Task views can still include childSessionKey, agentId, and runId; treat responses as operational data. 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.

Errors and troubleshooting

Response Next check
401 Use the route secret. An unresolved SecretRef leaves that route cold; repair it and reload/restart.
405 / 415 Send POST with Content-Type: application/json.
408 / 413 Send the JSON body within 15 seconds and below 256 KiB.
429 Reduce request rate or concurrent requests, including clients sharing a proxy.
400 invalid_request Check the action's fields and types; unknown fields are rejected.
404 not_found The mutation target does not exist in the route's bound session.
409 Inspect code: reconcile revisions, managed-flow status, cancellation state, or backing-run ownership.
503 persist_failed The record could not be persisted; investigate Gateway storage/logs before retrying.

Failures before action validation can be plain text, not the JSON envelope above. A Bearer header takes precedence over x-openclaw-webhook-secret; query-string and body tokens are not authentication methods for this plugin.