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 streamWebhooks
Best forBrowser UIs, long-lived server processesServerless functions, external system triggers
DeliveryPull (you consume the stream)Push (Clockwork calls your endpoint)
PersistenceLost if connection dropsRetried by Clockwork
AuthYour API keyHMAC-SHA256 signature
ReplayUse events.replay()Use events.list()