Anchored Scheduling
By default Clockwork schedules forward: each task starts as early as its dependencies allow. Anchored scheduling answers the opposite question — "it must all be done at 16:00; when does each step have to start?"
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.
Anchored scheduling needs the platform flag CLOCKWORK_PLANNING_V2. Forward scheduling is unchanged and remains the default.
Create an anchored job
const { data } = await client.workflowInstances.createWithGraph({
instance: {
name: 'Release',
anchor: { type: 'finish', at: '2026-11-26T16:00:00Z' },
schedulingDirection: 'backward',
},
tasks: [
{ ref: 'build', name: 'Build', durationMinutes: 60 },
{ ref: 'test', name: 'Test', durationMinutes: 30 },
{ ref: 'ship', name: 'Ship', durationMinutes: 20,
anchorWindow: { minOffsetMinutes: -30, maxOffsetMinutes: -5 } }, // finish 15:30–15:55
],
dependencies: [{ from: 'build', to: 'test' }, { from: 'test', to: 'ship' }],
})
data.schedule.requiredStart // when the earliest task must begin
data.schedule.makespanMinutes // required start -> last end
data.instance.requiredStart // same, stored on the instanceIn backward mode every unstarted task is placed at its latest start that still lets the plan finish by the anchor. Tasks that have started or completed (and isFixed tasks) never move.
What each task exposes
| Field | Meaning |
|---|---|
plannedStart / plannedEnd | where the plan placed the task |
earliestStart | the earliest it could start (your lower bound plus predecessors) |
latestStart | the latest it can start and still meet the anchor |
slackSec | latestStart − earliestStart |
bufferAfterMinutes counts toward the anchor: a task and its buffer must be done by then.
Constraints relative to the anchor
Two constraint types are understood relative to an instance's time references. Attach them to the job's tasks; a planner-managed job (schedulingDirection: 'backward' or resourceLeveling: 'delay') is recomputed immediately. On a forward job with leveling off they are not applied. (Attaching to the whole job with targetType: 'instance' currently fails with 500: see Constraints.)
// Finish between 30 and 5 minutes before the anchor (negative = before).
await client.constraints.create({
name: 'Ship window', type: 'anchor_window',
definition: { relativeTo: 'instance_anchor', minOffsetMinutes: -30, maxOffsetMinutes: -5 }, // applyTo: 'finish' | 'start'
})
// Finish no later than 60 minutes before the anchor, or N after the instance started.
await client.constraints.create({
name: 'Prep done', type: 'deadline',
definition: { relativeTo: 'instance_anchor', offsetMinutes: -60 }, // or 'instance_start'
})deadline accepts offsetMinutes, offsetHours and offsetDays (they add up). Definitions that name a relativeTo are validated (422 if malformed); deadline constraints without one stay free-form as before.
When it cannot be met
If the required start is already in the past, or a task cannot start early enough (for example it was delayed), you still get the best schedule — tasks pushed as little as needed, dependencies intact — plus an anchor_infeasible conflict stating the shortfall:
{ "type": "anchor_infeasible", "shortfallMinutes": 30,
"description": "The plan cannot meet its anchor: it is short by 30 minutes (the required start is already in the past)." }Notes
- Recompute is deterministic and idempotent: the same input gives the same schedule, and a recompute with nothing changed writes nothing new.
earliestStartis your lower bound for anchored instances and is never overwritten by planning. - Starting a task (
status: 'running') records where it started so it stays put; completing it does the same. - Task slack is stored as a 32-bit integer and is capped at ≈68 years.