Conflicts

A Conflict is a scheduling violation detected by the Clockwork engine. Conflicts are created automatically — you never create them manually. They surface when a deadline would be missed, a person has overlapping tasks, a resource is over-allocated, a plan cannot meet its anchor, a maximum lag is exceeded, or travel time between locations is insufficient.

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.

When to use client.conflicts

  • Building an operations dashboard that highlights at-risk workflows
  • Automating remediation — e.g., auto-delay a task when a conflict is detected
  • Snoozing non-critical conflicts to reduce noise
  • Integrating with alerting systems via the conflict.detected webhook event

Methods

MethodDescription
list(opts?)List conflicts (optionally filter by resolved status)
get(conflictId)Fetch a single conflict
resolve(conflictId)Mark as manually resolved
snooze(conflictId, input)Snooze until a future time

conflicts.list(opts?)

typescript
// Open (unresolved) conflicts in the project
const { data: open } = await client.conflicts.list({ resolved: false })
 
for (const conflict of open) {
  console.log(conflict.type, conflict.description, conflict.slackViolationSec)
}

Filter options

OptionTypeDescription
projectIdstringFilter to a specific project
resolvedbooleanfalse: only open conflicts. true: only resolved ones. Omitted: both

The query string accepts true, false, 1 and 0; anything else is a 400 VALIDATION_ERROR. (Servers released before this fix read resolved=false as true; on those, call list() and filter on resolvedAt yourself.)

conflicts.resolve(conflictId)

typescript
await client.conflicts.resolve('con_abc123')

conflicts.snooze(conflictId, input)

Records that you want to ignore the conflict until the given time (snoozedUntil) and writes a conflict.snoozed entry to the event log. The platform does not act on it: the conflict still appears in list() (including with resolved: false), and nothing re-raises it when the time passes. Filter on snoozedUntil in your own code if you want snoozed conflicts hidden.

typescript
await client.conflicts.snooze('con_abc123', {
  until: '2026-07-01T09:00:00Z',
})

Conflict types

TypeCause
deadline_violatedA task cannot complete before its deadline given current scheduling
resource_contentionOne person (the task's assignee) has overlapping tasks. Stored on the later task, with affectedActorId set and slackViolationSec = the overlap
travel_time_violatedConsecutive tasks require more travel time than is available between them
resource_capacity_breachA resource is over-allocated: occupied units exceed capacity at some moment, a claim falls outside the resource's availability windows, or two claims differ by more than the resource's shared-state tolerance (or need a changeover). See below. Older capability-based requirements can also raise it as Cannot complete before deadline — resource contention delays finish past the deadline. with slackViolationSec 0
constraint_violatedA constraint is broken: a max_concurrent group is over its cap, or a task is placed outside its anchor_window / deadline constraint
anchor_infeasibleA backward plan cannot meet its anchor. shortfallMinutes says by how much
max_lag_exceededA dependency's maxLagMinutes cannot be honored
capacity_breachA person's working-hours slots cannot fit the task before its deadline: Cannot complete before deadline — overloaded by N day(s). (slackViolationSec = N x 86,400), or, for a task due within a day when the assignee has no availability schedule, No availability schedule configured — cannot verify this task fits before its deadline. (slackViolationSec 0). Part of the older per-person capacity pass, unrelated to resource capacity

These eight values are the whole ConflictType union and the database enum; none has been removed. Which are produced by planning v2 (anchor_infeasible, max_lag_exceeded, and the occupancy-, window- and state-based form of resource_capacity_breach) is spelled out below.

resource_contention or resource_capacity_breach?

resource_contentionresource_capacity_breach
Abouta person: tasks with the same assignee that overlap in timea resource: units claimed through requirements exceed its capacity, or windows or state are broken
Units, occupancy, bookendsnot considered: one task = one slotconsidered
Set byassigneePlatformUserIdrequirements / setRequirements, and resourceLeveling can resolve it
affectedActorIdthe personnull

tasks.assignResource and resources.allocate raise neither (the engine can also compare tasks that hold the same directly allocated resource, but the platform does not currently pass that information to it).

Over-allocated resources (resource_capacity_breach)

When a task claims units of a resource (see Resource Requirements), the planner looks only at the occupied time of each claim: the whole task for full, only the first and last minutes for bookends, nothing for none. At every instant it adds up the units of the claims and compares the total with the resource's capacity. Each maximal stretch where the total is higher produces one conflict:

  • it is attached to the claim that started last in that stretch (the one that tipped it over; ties go to the smaller claim, then name, then id);
  • slackViolationSec is the length of the stretch in seconds;
  • other jobs that claim the same resource count toward the total.

A press with 2 slots and two runs that each need both slots, at the same time:

json
{
  "taskId": "…",
  "ref": "run-b",
  "type": "resource_capacity_breach",
  "description": "Resource is over capacity (2) while \"run-b\" runs.",
  "slackViolationSec": 3600
}

Availability and shared-state breaches use the same type with different descriptions ("Set up the press" needs the resource 60 minute(s) outside its availability.).

Contention is only detected, unless you opt in

By default (resourceLeveling: 'off') the planner does not move anything to avoid an over-allocation. The request succeeds, the tasks stay where they are, and the conflict is reported (in schedule.conflicts and in conflicts.list()). Treat a non-empty conflicts array as "this plan does not fit", not as success. To have the planner delay tasks until the resource is free, create the job with resourceLeveling: 'delay' (Resource Leveling).

Fixing it by hand when leveling is off

Find the conflict, delay the task it names, and look again. The conflict is stored on the task that tipped the resource over, so that is the one to move:

typescript
const minutesFromNow = (m: number) => new Date(Math.ceil(Date.now() / 60_000) * 60_000 + m * 60_000).toISOString()
type Found = { ref: string; type: string; description: string; slackViolationSec: number; shortfallMinutes?: number }
const types = (conflicts: Found[]) => conflicts.map((c) => c.type)
 
// ── 1. Contention is only DETECTED unless the job opts into leveling ─────────────────
// A press with 2 slots; two independent runs that each need both slots, both ready now.
const { data: off } = await client.workflowInstances.createWithGraph({
  instance: { name: `Press plan ${suffix}`, resourceLeveling: 'off' }, // 'off' is the default
  resources: [{ ref: 'press', name: `Press ${suffix}`, type: 'asset', capacity: 2 }],
  tasks: ['run-a', 'run-b'].map((ref) => ({
    ref, name: `${ref} ${suffix}`, durationMinutes: 60, earliestStart: minutesFromNow(60),
    requirements: [{ resourceRef: 'press', units: 2 }],
  })),
})
// 201: the request succeeds, the plan still has both runs at the same time, and the conflict is reported
if (types(off.schedule.conflicts).join() !== 'resource_capacity_breach') throw new Error('expected one breach')
const breach = off.schedule.conflicts[0]!
console.log(breach.type, '-', breach.description, `(${breach.slackViolationSec} s)`)
const starts = off.schedule.tasks.map((t) => t.plannedStart)
if (starts[0] !== starts[1]) throw new Error('nothing moved: both runs are still planned together')
 
// The same conflict is stored. `resolved: false` lists open conflicts only; keep the ones on this job's tasks.
const taskIds = Object.values(off.refs)
const open = async () => (await client.conflicts.list({ resolved: false })).data.filter((c) => taskIds.includes(c.taskId))
let stored = await open()
if (stored.length !== 1 || stored[0]!.taskId !== breach.taskId) throw new Error('expected the breach to be stored on the blamed task')
 
// ── 2. Manual remediation when leveling is off: delay the blamed task, then re-check ───
// Move it to start when the other run has finished (60 minutes later).
await client.tasks.delay(stored[0]!.taskId, { delayMinutes: 60, reason: 'Press is busy with the other run' })
stored = await open()
if (stored.length !== 0) throw new Error('expected the delay to clear the conflict: ' + JSON.stringify(stored))
console.log('after delay: no open conflicts')

Runnable: docs-examples/conflicts-detect-and-fix.mts. resolve only marks a conflict as handled; it does not change the plan, and a recompute that still finds the problem raises it again.

Planner conflicts with real responses

These come from createWithGraph (and simulate) in schedule.conflicts; the same conflicts are stored and listed with conflicts.list(). In the batch-graph response each conflict also carries the task's ref.

anchor_infeasible (plus constraint_violated for the anchor window). Three hours of work must finish within one hour of the anchor, and run must finish 5 to 30 minutes before it:

json
{ "ref": "run", "type": "constraint_violated",
  "description": "Task \"Print run\" is placed 155 minute(s) outside its allowed window.",
  "slackViolationSec": 9288 }
{ "ref": "plates", "type": "anchor_infeasible",
  "description": "The plan cannot meet its anchor: it is short by 150 minutes (tasks cannot start early enough).",
  "slackViolationSec": 8988, "shortfallMinutes": 150 }

anchor_infeasible is attached to the first task of the plan; shortfallMinutes is the shortfall rounded up to whole minutes. The best schedule is still returned, with dependencies intact. See Anchored Scheduling.

max_lag_exceeded. "Cut within 30 minutes of printing", but printing is fixed and the cut cannot start before 4 hours later:

json
{ "ref": "cut", "type": "max_lag_exceeded",
  "description": "\"Cut\" runs 150 minute(s) past the maximum lag of 30 minute(s) after \"Print\".",
  "slackViolationSec": 9000 }

It is attached to the later task, and slackViolationSec is how far past the limit it runs. If the earlier task is not fixed or started, the planner delays it to keep the limit and raises no conflict. See Dependencies.

deadline_violated: { "type": "deadline_violated", "description": "Task \"Print\" cannot finish by its deadline.", "slackViolationSec": 3588 } for two hours of work due in one hour (the seconds are how late it would finish).

typescript
// ── 3. Other conflict types, with real responses ────────────────────────────────────
// deadline_violated: 2 hours of work, due in 1 hour
const deadline = await client.workflowInstances.createWithGraph({
  tasks: [{ ref: 'a', name: `Print ${suffix}`, durationMinutes: 120, deadline: minutesFromNow(60) }],
})
if (types(deadline.data.schedule.conflicts).join() !== 'deadline_violated') throw new Error('expected deadline_violated')
 
// anchor_infeasible (+ constraint_violated for the anchor window): 3 hours of work must finish in 1 hour
const anchored = await client.workflowInstances.createWithGraph({
  instance: { anchor: { type: 'finish', at: minutesFromNow(60) }, schedulingDirection: 'backward' },
  tasks: [
    { ref: 'plates', name: `Make plates ${suffix}`, durationMinutes: 180 },
    { ref: 'run', name: `Print run ${suffix}`, durationMinutes: 30, anchorWindow: { minOffsetMinutes: -30, maxOffsetMinutes: -5, applyTo: 'finish' } },
  ],
  dependencies: [{ from: 'plates', to: 'run' }],
})
const kinds = types(anchored.data.schedule.conflicts).sort().join()
if (kinds !== 'anchor_infeasible,constraint_violated') throw new Error('unexpected: ' + kinds)
const infeasible = anchored.data.schedule.conflicts.find((c) => c.type === 'anchor_infeasible')!
if (infeasible.shortfallMinutes !== Math.ceil(infeasible.slackViolationSec / 60)) throw new Error('shortfallMinutes mismatch')
console.log('anchor_infeasible short by', infeasible.shortfallMinutes, 'minutes')
 
// max_lag_exceeded: "cut within 30 minutes of printing", but printing is fixed and the cut cannot start before +4 h
const lag = await client.workflowInstances.createWithGraph({
  tasks: [
    { ref: 'print', name: `Print ${suffix}`, durationMinutes: 60, isFixed: true, earliestStart: minutesFromNow(0) },
    { ref: 'cut', name: `Cut ${suffix}`, durationMinutes: 30, earliestStart: minutesFromNow(240) },
  ],
  dependencies: [{ from: 'print', to: 'cut', maxLagMinutes: 30 }],
})
if (types(lag.data.schedule.conflicts).join() !== 'max_lag_exceeded') throw new Error('expected max_lag_exceeded')
console.log(lag.data.schedule.conflicts[0]!.description)

Type reference

typescript
type ConflictType =
  | 'deadline_violated'
  | 'resource_contention'
  | 'travel_time_violated'
  | 'capacity_breach'
  | 'resource_capacity_breach'
  | 'constraint_violated'
  | 'anchor_infeasible'
  | 'max_lag_exceeded'
 
interface Conflict {
  id: string
  taskId: string
  type: ConflictType
  description: string
  affectedActorId: string | null
  slackViolationSec: number
  resolvedAt: string | null
  snoozedUntil: string | null
  notificationSent: boolean
  createdAt: string
}

Known limitations

  • Older SDK versions. @clockwork/sdk releases before the security-and-correctness fixes (#274, 2026-10-12) exported a ConflictType union without anchor_infeasible and max_lag_exceeded, so conflict.type === 'anchor_infeasible' did not compile; on those versions widen it, for example (conflict.type as string) === 'anchor_infeasible'.
  • Stale conflicts across jobs. A recompute rewrites the open conflicts of the job it ran for. If job A's change frees a resource that job B was over-allocating, B's stored conflict stays until B recomputes (any mutation in B).
  • Stored vs returned. A stored conflict has no ref or shortfallMinutes; those exist only in the plan response. tasks.createBatch returns no conflicts at all: read them with conflicts.list().
  • resource_contention is about a person with overlapping tasks. tasks.assignResource on two overlapping tasks does not raise it, whatever the resource's capacity: use requirements to model a shared resource.