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 secretX-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
| Event | Fires when |
|---|---|
task_ready | A task becomes ready (all predecessors completed) |
task_completed | A task is marked complete |
task_delayed | A task's start is pushed back |
conflict_detected | A scheduling conflict is detected |
workflow_completed | All tasks in an instance are done |
* | All of the above |
Next steps
- Event-Driven Patterns — using the SSE stream for real-time UIs
- Events SDK reference