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 main and 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 flag CLOCKWORK_PLANNING_V2 is 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 answer 404. 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 capacity at some moment, or outside its availability windows (resource_capacity_breach; the planner can resolve it with resourceLeveling: '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:

typescript
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:

typescript
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:

typescript
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:

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

ActionUse 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()).