Skip to content

Admin — Webhooks

Webhooks are custom HTTP endpoints that external systems can POST to in order to trigger an agent turn or a routine. They’re blank slates — any system that can make an authenticated HTTP POST can become a ThinkWork trigger through a webhook.

Route: /webhooks
File: apps/admin/src/routes/_authed/_tenant/webhooks/index.tsx

A searchable DataTable with:

ColumnNotes
NameDisplay name
TypeBadge: Agent or Routine depending on what the webhook targets
TargetAgent or routine name
EnabledEnabled / Disabled badge
Last InvokedRelative timestamp
Invocation countLifetime count
CreatedTimestamp

A New Webhook button opens the creation dialog.

Route: /webhooks/:webhookId
File: apps/admin/src/routes/_authed/_tenant/webhooks/$webhookId.tsx

  • Webhook name
  • Enable / disable toggle
  • Delete with confirmation
  • The bearer token (masked by default with a show / copy button)
  • The full invocation URL: https://<api-endpoint>/webhooks/<webhookId>

External systems call the URL with Authorization: Bearer <token> and a JSON body. The token is a random 32-character string generated at webhook creation and never rotated automatically.

Read-only display of:

  • Target type (agent / routine)
  • Target name
  • Rate limit (requests per minute)
  • Prompt override (for agent targets — the string the agent receives when the webhook fires)

An expandable table of run cards, one per invocation:

  • Status — succeeded / failed
  • Started at / finished at
  • Error message (for failures)
  • Context snapshot — the JSON payload the webhook received, used for replay and debugging
EndpointPurpose
GET /api/webhooksList webhooks
GET /api/webhooks/:webhookIdDetail
GET /api/webhooks/:webhookId/runsInvocation history
POST /api/webhooksCreate
PATCH /api/webhooks/:webhookIdUpdate
DELETE /api/webhooks/:webhookIdDelete
POST /api/webhooks/:webhookId/testManual test invocation

Once created, external systems trigger the webhook with a simple POST:

Terminal window
curl -X POST "https://<api-endpoint>/webhooks/<webhookId>" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message": "hello from external system", "context": {"foo": "bar"}}'

The backend:

  1. Validates the token
  2. Checks the rate limit (returns 429 if exceeded)
  3. Invokes the target (agent turn or routine execution)
  4. Writes a row to the invocation log with the full payload in context_snapshot
  5. Returns a response once the target starts (not once it finishes)

The webhook is fire-and-forget from the caller’s perspective. If the caller wants to know the result, they should read it from the created thread or poll the run status.

  • webhooks — Aurora table with id, tenant_id, name, description, token, target_type (agent / routine), agent_id / routine_id, prompt, config, enabled, rate_limit, last_invoked_at, invocation_count, created_at, updated_at
  • Runsthread_turns rows with webhook_id FK and a context_snapshot field storing the inbound JSON payload
  • Rate limit state — in-memory per webhook (per instance, not distributed); resets on Lambda cold start

Each webhook has a per-minute rate limit (default 10). When the limit is exceeded, the backend returns HTTP 429 without invoking the target and without writing to the run log. The rate limit is a hard cap — there’s no queue or back-pressure.

Because rate limit state is per-Lambda-instance, distributed invocations can collectively exceed the declared limit by the number of warm Lambda instances. For workflows that genuinely need hard caps, use a downstream queue rather than relying on the webhook’s rate limit.

For agent-targeted webhooks, the prompt field lets operators specify a canonical prompt that the agent receives when the webhook fires. The inbound JSON body is still available in context_snapshot and can be referenced by the agent’s tools, but the prompt override controls what the agent “hears” when the turn starts.

This is how operators ship “when something happens, do X” workflows: the prompt override says “do X” and the inbound payload carries the context.

  1. Click New Webhook
  2. Name it “Zapier → daily-summary agent”
  3. Target type: agent
  4. Target: the daily-summary agent
  5. Rate limit: 10/min
  6. Prompt override: “A new Zapier trigger fired. Review the payload and produce a summary.”
  7. Save
  8. Copy the token and the URL
  9. Paste into Zapier’s webhook action
  10. Test from Zapier — verify the invocation appears in the log
  1. Open the webhook detail page
  2. Scroll to the invocation log
  3. Expand the most recent failed row
  4. Check the error message
  5. Check context_snapshot to see what was actually received
  6. Common causes: token mismatch (401), rate limit (429), target agent not found or disabled (500), payload parse error (400)
  • No in-place token rotation. Rotation is delete + recreate.
  • Per-instance rate limiting. The rate limit is enforced per warm Lambda instance, not distributed. Distributed usage can collectively exceed the declared limit.
  • No queue. Rate-limited requests are rejected, not queued.
  • Fire-and-forget. The caller gets an immediate ack; the actual result lands on the thread created by the invocation.
  • REST-only. Webhooks don’t flow through urql; the list refetches after each mutation.
  • Automations — the time-based counterpart to webhook-driven triggers, plus the multi-step routines webhooks can launch
  • Agents — the other target type for webhook invocations
  • Automations (concept) — the product model