Conflict Resolution
Clockwork's scheduling engine automatically detects conflicts — deadline violations, a person's overlapping tasks, over-allocated resources, broken constraints, unreachable anchors, exceeded maximum lags and travel-time violations — after every task mutation. The Conflicts reference lists every type. This guide covers how to monitor for conflicts and respond to them programmatically.
Planning v2 availability. Merged to
mainand deployed to production (platform.clockwork-co.com; F1 to F7 and F9 to F11 on 2026-10-09, F12 pool assignment and the security fixes on 2026-10-12). The platform flagCLOCKWORK_PLANNING_V2is currently on globally, in production and in preview, as of 2026-10-12: every organization gets planning-v2 behavior, and with the flag off the planning-v2 endpoints answer404. Per-organization gating (CLOCKWORK_PLANNING_V2=orgs) is merged but not enabled in production, so the global flag is the only access control in effect. Exact dates and commits: Changelog.
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 person overlap in time (
resource_contention) - A resource is asked for more units than its
capacityat some moment, or outside its availability windows (resource_capacity_breach; the planner can resolve it withresourceLeveling: 'delay') - A rule is broken (
constraint_violated), a backward plan cannot meet its anchor (anchor_infeasible), or a dependency's maximum lag is exceeded (max_lag_exceeded) - 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:
await client.events.subscribe({
url: 'https://yourapp.com/webhooks/clockwork',
events: ['conflict.detected'],
})The event names the task but not the conflict (its data is { taskId, taskName, workflowId, workflowInstanceId, assigneePlatformUserId }), and it is sent again on every recompute that still finds a conflict. So in your webhook handler, read the task's open conflicts from the API and make the handling idempotent:
async function handleConflictEvent(event: { task_id?: string }) {
if (!event.task_id) return
const { data: task } = await client.tasks.get(event.task_id)
const { data: open } = await client.conflicts.list({ resolved: false })
for (const conflict of open.filter((c) => c.taskId === task.id)) {
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:
async function routeConflict(conflict: Conflict, task: Task) {
switch (conflict.type) {
case 'resource_contention':
// The same person has overlapping tasks. 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:
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:
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 |
Snoozing only records snoozedUntil on the conflict: conflicts.list() still returns it and nothing re-raises it when the time passes, so hide snoozed conflicts yourself (conflict.snoozedUntil && new Date(conflict.snoozedUntil) > new Date()).
Related
- Conflicts SDK reference
- Opportunities — positive scheduling signals
- Event-Driven Patterns — subscribe to
conflict.detected