Skip to main content
Event triggers allow runs and workflow steps to pause execution and wait for an external event to arrive via API. They enable durable human-in-the-loop processes, inter-service coordination, and reactive workflows that can wait days, weeks, or months without holding goroutines or consuming resources.

How It Works

  1. Create a trigger: A workflow step or SDK call creates an event trigger with a globally unique event_key and optional timeout.
  2. Wait: The step/run transitions to waiting status. No goroutine is held — the wait is a database row.
  3. External event arrives: An external system sends the event via POST /v1/events/{eventKey}/send with the matching event_key.
  4. Resume: The step/run resumes with the event payload as input data. Workflow progression continues automatically.

Trigger Types

Event Sources

Event triggers track where the wait originated:

Event Key

The event_key is a globally unique string controlled by the caller. It has a UNIQUE constraint in the database — only one waiting trigger can exist per key at a time.
Callers are responsible for namespacing event keys to avoid collisions. Use a pattern like {type}:{entity-id} (e.g., aml-check:user-123, payment:order-456).

Key Constraints

  • Maximum length: 512 characters
  • Must not be empty
  • Must not contain control characters (bytes below 0x20, including null bytes and newlines)
  • Must be unique across all waiting triggers
  • Template rendering supported (e.g., aml-check:{{payload.user_id}})
Validation is enforced at all API entry points — handleSendEvent, handleGetEventTrigger, handleCancelEventTrigger, handleEventTriggerStream, handleSendEventByPrefix, and handleSDKWaitForEvent — as well as during template rendering in the workflow engine.

Trigger Lifecycle

Timeout Behavior

Every event trigger has a timeout_secs value (default: 3600 seconds / 1 hour). When the timeout expires:
  • The trigger status transitions to timed_out
  • For workflow steps: the step transitions to failed and the workflow follows its on_failure policy (fail_workflow, skip_dependents, or continue)
  • For job runs: the run transitions to timed_out
The reaper process polls for expired triggers every 30 seconds.
Set timeout_secs to a large value for long-running waits (e.g., 604800 for 7 days). The wait consumes no resources — it’s just a database row.

Workflow Step: wait_for_event

Add a wait_for_event step to your workflow to pause until an external event arrives:

Step Fields

Template Rendering

The event_key supports template variables rendered from the workflow trigger payload:
  • {{payload.user_id}} → value of user_id from the trigger payload
  • {{payload.order.id}} → nested field access with dot notation

Workflow Step: sleep

Durable sleep steps pause a workflow for a specified duration without holding goroutines:
Sleep steps create an event trigger with trigger_type: sleep and an expiry time. When the reaper detects the expiry, it marks the trigger as received and completes the step — triggering workflow progression.

Supported Durations

Go duration strings: 30s, 5m, 1h, 24h, 168h (7 days), etc.

Event Chaining

Steps can auto-emit events on completion using event_emit_key. When the step completes, it automatically resolves a matching waiting trigger:
This enables cross-workflow coordination — one workflow’s step completion can resume another workflow’s waiting step.

SDK: Wait for Event

Job runs can also wait for events using the SDK endpoint:
When the event arrives, the run is re-queued with the event payload as checkpoint data and re-dispatched to your endpoint.

Request Fields

Sending Events

Transaction Safety

When sending an event that resolves a workflow step trigger, the API wraps both the trigger status update and the step completion in a single database transaction. If either operation fails, both roll back atomically — preventing inconsistency where a trigger is marked received but the step remains in waiting status. Fan-in progression (starting downstream steps) runs outside the transaction since it involves queues and multiple tables. The reconciliation reaper acts as a safety net for any edge cases. For job run sources, the existing ReceiveEventAndRequeueRun atomic path handles trigger update and run re-queue in a single operation.

By Exact Key

By Prefix (Batch)

Resolve all waiting triggers whose event key starts with a prefix:

Idempotent Resend

If an event has already been received for a key with the same payload, the API returns 200 OK with the existing trigger. If the payload differs, it returns 409 Conflict.

Canceling Triggers

Cancel a waiting trigger and drive progression (step fails, run cancels):

Webhook Notifications

When a trigger is created with a notify_url, the system sends a webhook when the event arrives. Delivery is persistent — it survives process restarts using a database-backed delivery queue.

Retry Strategy

  • Max attempts: 5
  • Backoff: Exponential — 5s, 25s, 125s, 625s (capped at 30 minutes)
  • 4xx errors: Dead-lettered immediately (not retryable)
  • 5xx / network errors: Retried up to max attempts

Webhook DLQ Management

Failed webhook deliveries can be inspected and retried via the API:
The retry endpoint resets the delivery to pending status with zero attempts and immediate next retry time. Only deliveries in failed status can be retried.

Real-Time Streaming

Stream trigger status changes via Server-Sent Events (SSE):
The stream receives:
  • Initial state on connect
  • Status updates on event receipt, timeout, or cancellation
  • Keepalive pings every 15 seconds
  • Automatic close on terminal state
The SSE stream uses a direct Redis pubsub channel (event_trigger:{id}) for sub-millisecond delivery. CDC also publishes to a project-level channel as a reliable catch-all.

Browser Authentication

The browser EventSource API cannot set custom headers. For browser-based SSE clients, pass the auth token as a query parameter:
The ?token= parameter is converted to an Authorization: Bearer header by the sseTokenAuth middleware before the standard auth middleware runs. Header-based auth continues to work unchanged — the query param is only used when no Authorization or X-Internal-Secret header is present.

Monitoring

Stats Endpoint

Returns counts per status:

List Triggers

Supports filtering by status, workflow_run_id, and source_type.

Per-Project Quotas

When a project has a MaxActiveEventTriggers quota configured, the SDK wait-for-event endpoint enforces the limit:
The quota only counts triggers in waiting status. Resolved, timed-out, and canceled triggers do not count toward the limit.

Data Retention & Purge

Terminal event triggers (received, timed_out, canceled) are automatically cleaned up by the reaper based on the EVENT_TRIGGER_RETENTION setting (default: 30 days). For manual control, use the purge API or CLI:
Or via the API:

SDK Client Libraries

Official SDKs:

TypeScript

Python

Reconciliation

A background reconciliation reaper runs every 30 seconds to catch edge cases:
  • Expired triggers: Triggers past their expires_at are timed out
  • Stale steps: Triggers marked received but whose step/run is still in waiting status (30-second grace period) are re-resolved
  • Expired approvals: Approval steps past their timeout are failed

Distributed Reaper

In multi-instance deployments, the reaper uses PostgreSQL advisory locks (pg_try_advisory_lock) to ensure exactly one instance runs the reaper cycle at a time. Other instances skip the cycle gracefully and try again on the next interval. This requires no external coordination service — the database itself provides leader election. If the active instance crashes, another instance acquires the lock on the next tick.

Prometheus Metrics

Data Model