Event-Driven Patterns
Clockwork exposes two event interfaces: a real-time SSE stream for browser and server applications, and webhooks for push delivery to your endpoint. This guide covers common patterns for both.
Pattern 1: Task inbox (SSE stream)
Build a real-time inbox that shows tasks becoming ready without polling:
typescript
import { Clockwork } from '@clockwork/sdk'
const client = new Clockwork({ apiKey: process.env.CLOCKWORK_API_KEY })
// Listen for newly-ready tasks and notify assignees
async function watchForReadyTasks() {
for await (const event of client.events.stream()) {
if (event.type === 'task.created' || event.type === 'task.updated') {
const taskId = (event.data as { taskId?: string }).taskId
if (!taskId) continue
const { data: task } = await client.tasks.get(taskId)
if (task.status === 'ready' && task.assigneePlatformUserId) {
await sendPushNotification(task.assigneePlatformUserId, {
title: 'New task ready',
body: task.name,
url: `/tasks/${task.id}`,
})
}
}
}
}Pattern 2: Cascade trigger (webhook)
Trigger a downstream system when a workflow milestone is reached:
typescript
// In your webhook handler
async function processEvent(payload: WebhookPayload) {
if (payload.event === 'task.completed') {
const { data: task } = await client.tasks.get(payload.task_id!)
// When the "Legal sign-off" task completes, send the contract
if (task.name === 'Legal sign-off') {
const { data: instance } = await client.workflowInstances.get(
task.workflowInstanceId!,
)
await sendContractToCounterparty({ dealId: instance.subjectId })
}
}
}Pattern 3: Conflict alerting
Subscribe to conflict_detected and page on-call when a critical-path task is at risk:
typescript
async function processEvent(payload: WebhookPayload) {
if (payload.event === 'constraint.violated') {
const conflicts = await client.conflicts.list({ resolved: false })
for (const conflict of conflicts.data) {
if (conflict.type === 'deadline_violated' && conflict.slackViolationSec > 3600) {
// More than 1 hour over deadline — escalate
await pageOnCall({
message: `Deadline violated: ${conflict.description}`,
severity: 'high',
taskId: conflict.taskId,
})
}
}
}
}Pattern 4: Event-sourced state reconstruction
Use events.replay() to rebuild your local state from the full event log after an outage:
typescript
async function rebuildState() {
let sequence = await getLastProcessedSequence()
while (true) {
const { data: events } = await client.events.replay({
fromSequence: sequence + 1,
limit: 500,
})
if (events.length === 0) break
for (const event of events) {
await applyEventToLocalState(event)
sequence = event.sequence
}
await saveLastProcessedSequence(sequence)
}
}Pattern 5: Workflow-per-subject fan-out
Create one workflow instance per incoming item and monitor them all:
typescript
// When orders come in, start a fulfillment workflow for each
async function onOrderReceived(orderId: string) {
const { data: instance } = await client.workflowInstances.create({
templateId: process.env.FULFILLMENT_TEMPLATE_ID,
subjectType: 'order',
subjectId: orderId,
})
console.log(`Started workflow ${instance.id} for order ${orderId}`)
}
// Monitor all in-progress orders
async function getDashboard() {
const { data: instances } = await client.workflowInstances.list({
status: 'running',
})
return instances.map(i => ({
orderId: i.subjectId,
status: i.status,
startedAt: i.startedAt,
}))
}Choosing SSE vs webhooks
| SSE stream | Webhooks | |
|---|---|---|
| Best for | Browser UIs, long-lived server processes | Serverless functions, external system triggers |
| Delivery | Pull (you consume the stream) | Push (Clockwork calls your endpoint) |
| Persistence | Lost if connection drops | Retried by Clockwork |
| Auth | Your API key | HMAC-SHA256 signature |
| Replay | Use events.replay() | Use events.list() |
Related
- Events SDK reference
- Webhooks guide — signature verification and retry handling