Conflict Resolution
Clockwork's scheduling engine automatically detects conflicts — deadline violations, resource contention, and travel-time violations — after every task mutation. This guide covers how to monitor for conflicts and respond to them programmatically.
How conflicts are created
You never create conflicts manually. The engine creates them when:
- A task's computed latest-finish exceeds its deadline (
deadline_violated) - Two tasks assigned to the same resource overlap in time (
resource_contention) - There is insufficient travel time between consecutive tasks at different locations (
travel_time_violated)
Monitoring via webhook
The fastest way to catch conflicts is to subscribe to the conflict_detected event:
typescript
await client.events.subscribe({
url: 'https://yourapp.com/webhooks/clockwork',
events: ['conflict_detected'],
})Then in your webhook handler:
typescript
async function handleConflict(conflictId: string) {
const { data: conflict } = await client.conflicts.get(conflictId)
const { data: task } = await client.tasks.get(conflict.taskId)
console.log(conflict.type, conflict.description)
console.log(`Slack violation: ${Math.round(conflict.slackViolationSec / 3600)}h`)
await routeConflict(conflict, task)
}Automated remediation
For simple cases, you can auto-remediate without human intervention:
typescript
async function routeConflict(conflict: Conflict, task: Task) {
switch (conflict.type) {
case 'resource_contention':
// Delay the lower-priority task by 2 hours
if (task.priority !== 'high') {
await client.tasks.delay(task.id, { delayMinutes: 120 })
await client.conflicts.resolve(conflict.id)
console.log(`Auto-delayed task ${task.name}`)
} else {
await escalateToManager(conflict)
}
break
case 'deadline_violated':
if (conflict.slackViolationSec < 3600) {
// Under 1 hour — snooze and monitor
await client.conflicts.snooze(conflict.id, {
until: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
})
} else {
// More than 1 hour over — escalate immediately
await escalateToManager(conflict)
}
break
case 'travel_time_violated':
// Insert a buffer task between the two conflicting tasks
await client.tasks.delay(task.id, { delayMinutes: 30 })
await client.conflicts.resolve(conflict.id)
break
}
}Human-in-the-loop resolution
For conflicts that require a human decision:
typescript
async function escalateToManager(conflict: Conflict) {
// Create a conversation on the conflicting task
const { data: conv } = await client.conversations.create({
parentType: 'task',
parentId: conflict.taskId,
title: `Conflict: ${conflict.type}`,
})
await client.conversations.postMessage(conv.id, {
role: 'system',
body: `A scheduling conflict was detected: ${conflict.description}. Slack violation: ${Math.round(conflict.slackViolationSec / 3600)}h. Please advise.`,
})
await sendSlackAlert({
channel: '#ops-escalations',
text: `Conflict on task requires attention. ${conflict.description}`,
conflictId: conflict.id,
})
}Bulk conflict review
To review and clear all non-critical conflicts in batch:
typescript
const { data: conflicts } = await client.conflicts.list({ resolved: false })
for (const conflict of conflicts) {
const { data: task } = await client.tasks.get(conflict.taskId)
if (task.priority === 'low' && conflict.slackViolationSec < 1800) {
// Low-priority task, small violation — auto-resolve
await client.tasks.delay(task.id, { delayMinutes: 60 })
await client.conflicts.resolve(conflict.id)
}
}Snooze vs resolve
| Action | Use when |
|---|---|
resolve() | The conflict has been genuinely fixed (task rescheduled, resource freed) |
snooze(until) | The situation will clear itself and you don't want the noise until then |
A snoozed conflict re-surfaces automatically when the snooze time passes if it hasn't been resolved.
Related
- Conflicts SDK reference
- Opportunities — positive scheduling signals
- Event-Driven Patterns — subscribe to
conflict_detected