Build durable human-in-the-loop workflows and inter-service coordination with event triggers.
Event triggers let you pause workflows and job runs to wait for external input — approvals, webhook callbacks, third-party processing results, or any asynchronous signal. This guide walks through common patterns.
Pattern 1: Human Approval with External System
A KYC pipeline that waits for an external AML check result:
{
"name": "KYC Onboarding",
"slug": "kyc-onboarding",
"steps": [
{
"step_ref": "collect-data",
"job_id": "job_collect_user_data"
},
{
"step_ref": "aml-check",
"type": "wait_for_event",
"event_key": "aml:{{payload.user_id}}",
"timeout_secs": 86400,
"depends_on": ["collect-data"]
},
{
"step_ref": "create-account",
"job_id": "job_create_account",
"depends_on": ["aml-check"]
}
]
}When the AML provider completes their check, they call your webhook endpoint. Your endpoint then forwards the result to strait:
curl -X POST https://strait.dev/v1/events/aml:user-456/send \
-H "Authorization: Bearer strait_..." \
-H "Content-Type: application/json" \
-d '{
"payload": {"result": "clear", "risk_score": 0.05}
}'The create-account step starts automatically with the AML result available via {{parent_outputs.aml-check.result}}.
Pattern 2: Multi-Stage Deployment with Manual Gates
{
"name": "Production Deploy",
"slug": "prod-deploy",
"steps": [
{"step_ref": "build", "job_id": "job_build"},
{"step_ref": "test", "job_id": "job_test", "depends_on": ["build"]},
{
"step_ref": "staging-deploy",
"job_id": "job_deploy_staging",
"depends_on": ["test"]
},
{
"step_ref": "qa-signoff",
"type": "wait_for_event",
"event_key": "qa-signoff:{{payload.release_id}}",
"timeout_secs": 604800,
"depends_on": ["staging-deploy"]
},
{
"step_ref": "prod-deploy",
"job_id": "job_deploy_prod",
"depends_on": ["qa-signoff"]
}
]
}QA team signs off via API or a custom dashboard that calls the send endpoint. The workflow can wait up to 7 days without consuming resources.
Pattern 3: Cross-Workflow Coordination
Use event_emit_key to chain workflows. When one workflow completes a step, it auto-resolves a trigger in another workflow:
Workflow A (data processing):
{
"steps": [
{
"step_ref": "process-batch",
"job_id": "job_process",
"event_emit_key": "batch-done:{{payload.batch_id}}"
}
]
}Workflow B (aggregation — triggered separately):
{
"steps": [
{
"step_ref": "wait-for-batch",
"type": "wait_for_event",
"event_key": "batch-done:{{payload.batch_id}}"
},
{
"step_ref": "aggregate",
"job_id": "job_aggregate",
"depends_on": ["wait-for-batch"]
}
]
}When Workflow A's process-batch step completes, it auto-emits an event that resumes Workflow B's wait-for-batch step.
Pattern 4: Durable Sleep
Pause a workflow for a specific duration:
{
"steps": [
{"step_ref": "send-email", "job_id": "job_send_welcome_email"},
{
"step_ref": "wait-24h",
"type": "sleep",
"sleep_duration": "24h",
"depends_on": ["send-email"]
},
{
"step_ref": "send-followup",
"job_id": "job_send_followup",
"depends_on": ["wait-24h"]
}
]
}The sleep step creates a trigger with an expiry time. The reaper completes it when the time arrives. No goroutines are held.
Pattern 5: SDK Wait for Event
Job runs can pause mid-execution and wait for an external event:
# Inside your job executor
import requests
# Report progress
requests.post(f"{STRAIT_URL}/sdk/v1/runs/{run_id}/progress",
headers={"Authorization": f"Bearer {run_token}"},
json={"percent": 50, "message": "Waiting for manual review"})
# Pause and wait for external event
requests.post(f"{STRAIT_URL}/sdk/v1/runs/{run_id}/wait-for-event",
headers={"Authorization": f"Bearer {run_token}"},
json={
"event_key": f"review:{document_id}",
"timeout_secs": 172800,
"notify_url": "https://myapp.com/webhook/review-ready"
})When the event arrives, the run is re-queued and re-dispatched with the event payload as checkpoint data. Your job code can read the checkpoint data on re-dispatch.
Pattern 6: Batch Resolution
Resolve multiple triggers at once using prefix matching:
# Approve all pending items in a batch
curl -X POST https://strait.dev/v1/events/prefix/batch-42:/send \
-H "Authorization: Bearer strait_..." \
-H "Content-Type: application/json" \
-d '{
"payload": {"approved": true}
}'This resolves all waiting triggers with keys starting with batch-42: (e.g., batch-42:item-1, batch-42:item-2, etc.).
Monitoring Triggers
CLI
# List waiting triggers
strait triggers list --project proj_1 --status waiting
# Get a specific trigger
strait triggers get aml:user-456
# Send an event
strait triggers send aml:user-456 --payload '{"result": "clear"}'Real-Time SSE Stream
Monitor a trigger in real-time:
curl -N https://strait.dev/v1/events/aml:user-456/stream \
-H "Authorization: Bearer strait_..."The stream closes automatically when the trigger reaches a terminal state.
Error Handling
Timeout Handling
When a trigger times out, the workflow follows its on_failure policy:
fail_workflow(default): The entire workflow failsskip_dependents: Downstream steps are skipped, other branches continuecontinue: The timeout is ignored, dependents proceed
Cancellation
Cancel a trigger to immediately fail the associated step:
curl -X DELETE https://strait.dev/v1/events/aml:user-456 \
-H "Authorization: Bearer strait_..."Webhook Delivery Failures
If a notify_url is configured and delivery fails:
- The delivery is persisted to the database
- Retried with exponential backoff (5s, 25s, 125s, 625s)
- After 5 failed attempts, the delivery is dead-lettered
- 4xx responses are dead-lettered immediately
Best Practices
- Use descriptive, namespaced event keys:
{type}:{entity-id}pattern prevents collisions (e.g.,kyc:user-123,deploy:release-v2.1) - Set appropriate timeouts: Use generous timeouts for human workflows (days/weeks) — the wait is free. Avoid infinite waits in production
- Handle timeout failures: Configure
on_failure: skip_dependentsorcontinuefor non-critical wait steps - Use event chaining for cross-workflow coordination:
event_emit_keyis more reliable than external HTTP calls between workflows - Monitor waiting triggers: Set up alerts for triggers approaching their timeout
- Use prefix patterns for batch operations: Design event keys with a common prefix when items are part of the same batch
Pattern 7: TypeScript SDK Client
Use the TypeScript SDK package @strait/ts for type-safe event trigger operations:
import { createClient } from "@strait/ts";
const runClient = createClient({
baseUrl: "https://strait.dev",
auth: { type: "runToken", token: process.env.RUN_TOKEN! },
});
const eventClient = createClient({
baseUrl: "https://strait.dev",
auth: { type: "bearer", token: process.env.STRAIT_API_KEY! },
});
// Inside your job executor — pause and wait for human review
const trigger = await runClient.operationsPromise.postSdkV1RunsByRunIDWaitForEvent({
pathParams: { runID },
body: {
event_key: `review:${documentId}`,
timeout_secs: 172800,
notify_url: "https://myapp.com/webhook/review-ready",
},
});
// In your review dashboard — approve the document
const resolved = await eventClient.operationsPromise.postV1EventsByEventKeySend({
pathParams: { eventKey: `review:${documentId}` },
body: { payload: { approved: true, reviewer: "alice", comments: "Looks good" } },
});
// Check trigger status
const status = await eventClient.operationsPromise.getV1EventsByEventKey({
pathParams: { eventKey: `review:${documentId}` },
});
console.log(status.status); // "received"Pattern 8: Data Retention & Purge
For compliance or cost management, periodically purge old terminal triggers:
# Preview what would be deleted
strait triggers purge --older-than 90 --dry-run
# Purge triggers older than 90 days
strait triggers purge --older-than 90The reaper also automatically cleans up based on EVENT_TRIGGER_RETENTION (default: 30 days). Adjust for your compliance requirements:
# Keep triggers for 1 year
export EVENT_TRIGGER_RETENTION=8760h
# Or use the legacy days-based setting
export EVENT_TRIGGER_RETENTION_DAYS=365Pattern 9: Browser SSE Dashboard
Build a real-time dashboard that monitors trigger status using Server-Sent Events:
<script>
// Browser EventSource API cannot set custom headers,
// so pass the auth token as a query parameter
const source = new EventSource(
"/v1/events/aml:user-123/stream?token=strait_your_api_key"
);
source.addEventListener("status", (event) => {
const trigger = JSON.parse(event.data);
document.getElementById("status").textContent = trigger.status;
if (["received", "timed_out", "canceled"].includes(trigger.status)) {
source.close(); // Terminal state — stop streaming
}
});
source.addEventListener("keepalive", () => {
console.log("Connection alive");
});
</script>