Constraints
A Constraint is a reusable scheduling rule that you attach to tasks. When the plan breaks it, Clockwork raises a constraint_violated Conflict. A constraint has a free-form type string, but only three types are interpreted; any other type is stored and returned as you sent it and never evaluated.
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.
When to use client.constraints
- Keeping a task inside a window measured from a job's required finish time (
anchor_window) - Giving tasks a deadline measured from the job's anchor or from when the job was created (
deadline) - Limiting how many of a set of tasks may run at the same time (
max_concurrent), for example "at most two press runs at once" - Reusing one rule across many tasks or jobs
Methods
| Method | Description |
|---|---|
create(input) | Define a constraint |
list(opts?) | List constraints in a project, newest first |
attach(constraintId, input) | Attach to a task (or, see below, a job) |
setStatus(constraintId, status) | 'active' or 'disabled' |
constraints.create(input)
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Human-readable constraint name |
type | string | yes | 'anchor_window', 'deadline' and 'max_concurrent' are interpreted; anything else is only stored |
definition | Record<string, unknown> | no | Type-specific configuration (below) |
projectId | string | no | Defaults to your personal project |
Which types do something
type | Interpreted by | Definition | Result when broken |
|---|---|---|---|
anchor_window | the planner, for jobs that use it | { relativeTo: 'instance_anchor', minOffsetMinutes?, maxOffsetMinutes?, applyTo?: 'finish' | 'start' } | constraint_violated ("placed N minute(s) outside its allowed window") |
deadline with a relativeTo | the planner, for jobs that use it | { relativeTo: 'instance_anchor' | 'instance_start', offsetMinutes?, offsetHours?, offsetDays? } | constraint_violated, same wording |
max_concurrent | the scheduling engine, on every recompute | { maxConcurrent: n } with an integer n >= 1 | constraint_violated on each task that pushes the concurrent count over n |
deadline without relativeTo, time_window, capacity, anything else | nothing | whatever you like | none: stored, listed, never evaluated |
"The planner, for jobs that use it" means a job created with schedulingDirection: 'backward' or resourceLeveling: 'delay' (see Workflow Instances). On a plain forward job with leveling off, anchor_window and deadline constraints are not applied when the job is recomputed. max_concurrent does not depend on that.
anchor_window
Keeps a task's finish (default) or start inside a window around the job's anchor, the required finish time set with instance.anchor. Offsets are in minutes; negative means before the anchor.
const { data: shipWindow } = await client.constraints.create({
name: 'Ship window',
type: 'anchor_window',
definition: { relativeTo: 'instance_anchor', minOffsetMinutes: -30, maxOffsetMinutes: -5 }, // finish 30 to 5 minutes before the anchor
})The definition is checked when you create the constraint, and is strict: relativeTo must be 'instance_anchor', at least one of minOffsetMinutes / maxOffsetMinutes is required, minOffsetMinutes <= maxOffsetMinutes, applyTo is 'finish' or 'start', and any other key is rejected. A bad definition is 422 VALIDATION_ERROR with a message such as Invalid anchor_window definition: Provide minOffsetMinutes and/or maxOffsetMinutes. If the job has no anchor, the constraint does not apply.
deadline
The task must finish by a time measured from a reference. The offsets add up (offsetDays: 1, offsetHours: 2 is 26 hours); each is an integer and defaults to 0.
// Done within 90 minutes of the job being created
const { data: within } = await client.constraints.create({
name: 'Done within 90 minutes',
type: 'deadline',
definition: { relativeTo: 'instance_start', offsetMinutes: 90 },
})
// Done 60 minutes before the anchor
await client.constraints.create({ name: 'Prep done', type: 'deadline', definition: { relativeTo: 'instance_anchor', offsetMinutes: -60 } })'instance_anchor'is the job's required finish time; without an anchor the constraint does not apply.'instance_start'is the moment the job was created.- A definition that names a
relativeTois validated (422 VALIDATION_ERRORfor another value, e.g.Invalid enum value. Expected 'instance_anchor' | 'instance_start', received 'instance_end'). Other keys are tolerated. Adeadlinewith norelativeTo(the earlier free-form style,{ offsetDays: 5 }) is stored as given and is not interpreted. - To put a deadline on a single task, use the task's own
deadlinefield: it raises adeadline_violatedconflict instead.
max_concurrent
const { data: cap } = await client.constraints.create({
name: 'At most 2 press runs at once',
type: 'max_concurrent',
definition: { maxConcurrent: 2 },
})
for (const taskId of [runA, runB, runC]) await client.constraints.attach(cap.id, { targetType: 'task', targetId: taskId })Every time the schedule is recomputed, the engine sweeps the attached tasks (skipping completed and cancelled ones). A task that starts while maxConcurrent others are running gets a constraint_violated conflict: Task "Run a" pushes this constraint's concurrent count to 3, over its cap of 2. Nothing is moved: it is reported, not resolved. A task that finishes at the exact moment another starts does not overlap it. A definition without a valid integer maxConcurrent is ignored.
constraints.attach(constraintId, input)
await client.constraints.attach(shipWindow.id, { targetType: 'task', targetId: 'tsk_xyz' }) // 201targetType | Effect |
|---|---|
'task' | The constraint applies to that task. This is the target to use |
'instance' | Documented to apply to every task of a job. Known issue: on a database built from the migrations the call currently fails with 500 INTERNAL_ERROR, because a database CHECK on constraint_attachments.target_type only allows execution, task and workflow_definition. Attach to the job's tasks instead |
'execution', 'workflow_definition' | Accepted and stored, never evaluated |
An unknown targetType is 400 VALIDATION_ERROR; an unknown constraint is 404 NOT_FOUND. targetId is not checked against any table. Attaching an anchor_window or deadline to a task of a planner-managed job recomputes that job straight away; for max_concurrent the violation appears on the next recompute (any task change). A task can carry several constraints: their windows are combined into the tightest one.
Graph tasks can carry the anchor window directly: anchorWindow on a task in createWithGraph creates an anchor_window constraint attached to that task for you.
constraints.setStatus(constraintId, status)
await client.constraints.setStatus(cap.id, 'disabled')A disabled constraint is no longer evaluated. Conflicts it already raised stay until the job's next recompute, which no longer produces them. Both changes are written to the event log (constraint.disabled, constraint.enabled), along with constraint.created and constraint.attached; these have no webhook.
Violations
A broken constraint produces a constraint_violated conflict on the task concerned, listed by client.conflicts.list() and, for planner jobs, returned in schedule.conflicts. The conflict.detected webhook announces it without the conflict itself; read it from the API.
Type reference
type ConstraintStatus = 'active' | 'disabled'
type ConstraintTargetType = 'execution' | 'task' | 'workflow_definition' | 'instance'
interface Constraint {
id: string
projectId: string | null
name: string
type: string
definition?: Record<string, unknown> | null
status?: ConstraintStatus
createdAt: string
}
interface CreateConstraintInput {
projectId?: string
name: string
type: string
definition?: Record<string, unknown>
}
interface AttachConstraintInput {
targetType: ConstraintTargetType
targetId: string
}Runnable: docs-examples/constraints-types.mts (every validation message above, a deadline violation, a max_concurrent violation, disabling, and the instance known issue).
Related
- Anchored Scheduling — the anchor these constraints are measured from
- Conflicts — constraint violations appear here
- Opportunities — scheduling windows opened by constraints