Webhooks

Webhooks let Clockwork push events to your server instead of you polling the API. When a task completes, a deadline is missed, or a workflow finishes, Clockwork sends a signed HTTP POST to your endpoint.

1. Register an endpoint

typescript
const { data: sub } = await client.events.subscribe({
  url: 'https://yourapp.com/webhooks/clockwork',
  events: ['task_completed', 'task_delayed', 'conflict_detected', 'workflow_completed'],
})
 
// Save the secret — it's only shown once at creation
console.log('Secret:', sub.secret) // whsec_abc123...

Store sub.secret in your environment variables. You'll use it to verify every incoming request.

2. Verify the signature

Every webhook request includes:

  • X-Clockwork-Signature: sha256=<hex> — HMAC-SHA256 of the raw body using your secret
  • X-Clockwork-Event: <event-name> — the event type

Always verify before processing. An invalid signature means the request is not from Clockwork.

typescript
import { createHmac, timingSafeEqual } from 'crypto'
 
function verifyClockworkSignature(
  signature: string,
  secret: string,
  rawBody: string,
): boolean {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex')
  // Use timingSafeEqual to prevent timing attacks
  return timingSafeEqual(
    Buffer.from(signature, 'utf8'),
    Buffer.from(expected, 'utf8'),
  )
}

3. Handle events

typescript
// Express handler (raw body parsing required for signature verification)
import express from 'express'
 
const app = express()
 
app.post(
  '/webhooks/clockwork',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-clockwork-signature'] as string
    const rawBody = req.body.toString('utf8')
 
    if (!verifyClockworkSignature(signature, process.env.CLOCKWORK_WEBHOOK_SECRET!, rawBody)) {
      return res.status(401).json({ error: 'Invalid signature' })
    }
 
    // Respond immediately — do heavy processing asynchronously
    res.status(200).send('OK')
 
    const payload = JSON.parse(rawBody)
    processEvent(payload).catch(console.error)
  }
)
 
async function processEvent(payload: {
  event: string
  task_id?: string
  workflow_id?: string
  timestamp: string
  inputs?: Record<string, unknown>
}) {
  switch (payload.event) {
    case 'task.completed':
      await onTaskCompleted(payload.task_id!)
      break
    case 'task.updated': // delayed
      await onTaskDelayed(payload.task_id!)
      break
    case 'workflow.completed':
      await onWorkflowCompleted(payload.workflow_id!)
      break
    case 'constraint.violated':
      await onConflict(payload)
      break
  }
}

Webhook payload shape

typescript
interface WebhookPayload {
  event: string             // dot-notation: 'task.completed', 'workflow.completed', etc.
  task_id?: string          // present for task events
  workflow_id?: string      // present for workflow events
  executor?: {
    platform_user_id: string | null
  }
  inputs?: Record<string, unknown>  // event-specific data
  timestamp: string         // ISO 8601 UTC
}

Idempotency

Clockwork may deliver the same event more than once (network retries, transient failures). Make your handler idempotent by recording processed event IDs:

typescript
const processedEvents = new Set<string>()
 
async function processEvent(payload: WebhookPayload & { id?: string }) {
  const eventId = payload.id ?? `${payload.event}:${payload.task_id}:${payload.timestamp}`
  if (processedEvents.has(eventId)) return // already handled
  processedEvents.add(eventId)
 
  // ... handle event
}

In production, use a database or Redis to persist the set.

Managing subscriptions

typescript
// List active subscriptions
const { data: subs } = await client.events.listSubscriptions()
 
// Delete a subscription
await client.events.unsubscribe('sub_abc123')

Wildcard subscription

Subscribe to all events with '*':

typescript
await client.events.subscribe({
  url: 'https://yourapp.com/webhooks/clockwork',
  events: ['*'],
})

Available events

EventFires when
task_readyA task becomes ready (all predecessors completed)
task_completedA task is marked complete
task_delayedA task's start is pushed back
conflict_detectedA scheduling conflict is detected
workflow_completedAll tasks in an instance are done
*All of the above

Next steps