How It Works
- Create a trigger: A workflow step or SDK call creates an event trigger with a globally unique
event_keyand optional timeout. - Wait: The step/run transitions to
waitingstatus. No goroutine is held — the wait is a database row. - External event arrives: An external system sends the event via
POST /v1/events/{eventKey}/sendwith the matchingevent_key. - 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
Theevent_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.
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}})
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 atimeout_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
failedand the workflow follows itson_failurepolicy (fail_workflow,skip_dependents, orcontinue) - For job runs: the run transitions to
timed_out
Workflow Step: wait_for_event
Add await_for_event step to your workflow to pause until an external event arrives:
Step Fields
Template Rendering
Theevent_key supports template variables rendered from the workflow trigger payload:
{{payload.user_id}}→ value ofuser_idfrom 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: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 usingevent_emit_key. When the step completes, it automatically resolves a matching waiting trigger:
SDK: Wait for Event
Job runs can also wait for events using the SDK 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 markedreceived 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 returns200 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 anotify_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: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):- Initial state on connect
- Status updates on event receipt, timeout, or cancellation
- Keepalive pings every 15 seconds
- Automatic close on terminal state
event_trigger:{id}) for sub-millisecond delivery. CDC also publishes to a project-level channel as a reliable catch-all.
Browser Authentication
The browserEventSource API cannot set custom headers. For browser-based SSE clients, pass the auth token as a query parameter:
?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
List Triggers
status, workflow_run_id, and source_type.
Per-Project Quotas
When a project has aMaxActiveEventTriggers quota configured, the SDK wait-for-event endpoint enforces the limit:
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 theEVENT_TRIGGER_RETENTION setting (default: 30 days).
For manual control, use the purge API or CLI:
SDK Client Libraries
Official SDKs:- TypeScript: strait-dev/strait-ts (
@strait/ts) - Python: strait-dev/strait-python
- Go: strait-dev/strait-go
TypeScript
Python
Reconciliation
A background reconciliation reaper runs every 30 seconds to catch edge cases:- Expired triggers: Triggers past their
expires_atare timed out - Stale steps: Triggers marked
receivedbut whose step/run is still inwaitingstatus (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.