Webhooks
Workflows can receive events from external systems via a webhook endpoint. This enables integration with approval systems, payment processors, CI/CD pipelines, and any other service that can send HTTP requests.
The Webhook Endpoint
External events are sent to:
POST /workflows/api/webhook/{conversation_id}/{state_name}The state_name parameter specifies the workflow state the conversation is expected to be in. If the conversation’s active workflow run is in a different state, the request is rejected with a 409 Conflict response.
The request body is JSON:
{
"event_key": "approval_granted",
"data": {
"approver": "jane@example.com",
"comment": "Looks good, approved."
}
}| Field | Required | Description |
|---|---|---|
| event_key | Yes | A string identifier that matches an external_event guard on a transition |
| data | No | Optional key-value data to merge into the workflow’s gathered data |
The endpoint returns 200 OK on success (with the new state if a transition fired), 401 Unauthorized if the token is invalid, 404 Not Found if the conversation does not exist, and 409 Conflict if the conversation is not in the expected state.
Authentication
Webhook requests are authenticated using a reply token. Each workflow conversation generates a unique reply token when it enters a state with an external_event guard. The token can be provided in two ways:
- Authorization header –
Authorization: Bearer <token> - Query parameter –
?token=<token>
The token is:
- Included in any outbound notifications sent by the workflow (e.g., approval request emails)
- Available in gathered data as
reply_tokenfor use in HTTP tool calls - Scoped to the specific conversation – it cannot be reused for other workflows
There is no separate API key or shared secret. The reply token serves as both identifier and credential.
Triggering Transitions
When the webhook receives a valid event:
- The
event_keyis stored in the workflow run’s gathered data - Any additional
datain the request body is merged into gathered data - The engine evaluates
external_eventguards on the current state’s outgoing transitions - If a matching guard is found and all other guards on that transition also pass, the workflow advances
- The response includes the new state name if a transition fired, or a
no_transitionindicator if no guard matched
Example: Approval System
A typical approval integration works like this:
- The workflow reaches a Request Approval state
- The assistant sends an approval request email to the manager, including a link with the
reply_token - The manager clicks “Approve” or “Deny” in the external approval system
- The approval system calls the webhook:
POST /workflows/api/webhook/{conversation_id}/request_approval?token=abc123...{
"event_key": "approval_granted",
"data": {
"approver": "manager@example.com",
"approved_at": "2026-03-31T10:00:00Z"
}
}- The
external_eventguard on the “Request Approval -> Update Calendar” transition fires - The workflow advances and the assistant proceeds with the next step
For the denial path, a separate transition with external_event guard matching approval_denied routes to a terminal state.
require_human_approval: true on a state and the workflow engine handles the approval UI, email notifications, and resume/reject flow automatically.Technical Details
Reply token generation – Reply tokens are generated as URL-safe random strings when the workflow engine detects that the current state has an outgoing transition with an external_event guard. The token is stored on the conversation record and rotated each time the workflow enters a new state that expects external events.
Event storage – Incoming events are stored in the workflow_events table with the conversation ID, event type, data payload, and timestamp. This provides an audit trail and allows events that arrive early (before the workflow reaches the waiting state) to be replayed when the state is entered.
Idempotency – Duplicate events (same reply token and event type) are deduplicated. The first event triggers the transition; subsequent duplicates are acknowledged with 200 but do not re-trigger.