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

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

A snoozed conflict re-surfaces automatically when the snooze time passes if it hasn't been resolved.