Tasks

A Task is the atomic unit of work in Clockwork. Tasks belong to a WorkflowInstance, have a duration and optional deadline, and flow through a lifecycle: pending → ready → running → completed (or blocked, failed, cancelled).

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.

The scheduling engine automatically re-computes start times, critical path, and conflicts after every task mutation.

When to use client.tasks

  • Creating tasks dynamically as your system processes new work items
  • Building a human task inbox that polls for ready tasks assigned to a user
  • Completing or delaying tasks from a webhook handler or automation script
  • Monitoring task slack (slackSec) to identify scheduling risk

Methods

MethodDescription
create(input)Add a task to a workflow instance
get(taskId)Fetch a task with computed schedule fields
list(opts?)List tasks filtered by instance or status
update(taskId, input)Update name, duration, status, dates, tags, priority
delete(taskId)Delete task; downstream tasks reschedule automatically
complete(taskId)Mark done; returns newly unlocked downstream tasks
assign(taskId, userId)Assign to a platform user
assignResource(taskId, resourceId)Assign a resource (equipment, agent, service)
delay(taskId, input)Delay by minutes or to a new earliest start; slip propagates downstream
createBatch(input)Create up to 500 tasks in one atomic request
setRequirements(taskId, requirements)Replace the task's units-and-occupancy claims on resources
listRequirements(taskId)Read the task's claims

createBatch, setRequirements and listRequirements answer 404 while the platform flag CLOCKWORK_PLANNING_V2 is off. tasks.simulate(input) (a dry run for one proposed task, POST /tasks/simulate) also exists; whole-job what-ifs are in Schedule Simulation.

tasks.create(input)

Parameters

ParameterTypeRequiredDescription
namestringyesHuman-readable task name
workflowInstanceIdstringnoBind to a running workflow instance
durationMinutesinteger >= 0noEstimated duration in minutes, default 0
durationSecondsinteger >= 0noFine-grained duration override
earliestStartstringnoISO 8601 (with Z or an offset); lower bound on the start, defaults to now
deadlinestringnoISO 8601; a task that cannot finish by it gets a deadline_violated conflict
isFixedbooleannoIf true, start time never moves regardless of dependencies
priority'high' | 'medium' | 'low'noDisplay priority (does not affect scheduling)
assigneePlatformUserIdstringnoAssign on creation
tagsstring[]noFreeform labels
bufferAfterMinutesnumbernoCooldown time after the task finishes
subjectTypestringnoOpaque label for external entity type (e.g., 'customer')
subjectIdstringnoExternal entity ID

Returns

Promise<ApiResponse<Task>> — data is the created Task (201).

Errors

StatuscodeCause
400VALIDATION_ERRORBody fails the schema (empty name, a non-ISO date, a negative duration, a non-UUID id, a priority other than high/medium/low)
400NOT_ASSIGNABLEThe actorId you gave cannot be resolved to an assignable person
403SLOT_NOT_PERMITTEDslotId is set and the assignee has not granted that slot to you
401 / 403UNAUTHORIZED / FORBIDDENMissing or invalid credential; a key without tasks:write

Example

typescript
import { Clockwork } from '@clockwork/sdk'
 
const client = new Clockwork({ apiKey: process.env.CLOCKWORK_API_KEY })
 
const { data: task } = await client.tasks.create({
  name: 'Review contract draft',
  workflowInstanceId: 'wi_abc123',
  durationMinutes: 30,
  priority: 'high',
  deadline: '2026-07-01T17:00:00Z',
  tags: ['legal', 'q3'],
})
 
console.log(task.id, task.status) // tsk_... 'pending'

tasks.createBatch(input)

Creates many tasks in one request: one transaction and one schedule computation for the batch. It counts as one request toward rate limits. To create tasks and dependencies and resources together, use Batch Graphs (workflowInstances.createWithGraph).

Parameters

ParameterTypeRequiredDescription
tasksarrayyes1 to 500 tasks
workflowInstanceIdstringnoPut every task in this instance (404 NOT_FOUND if it does not exist)
projectIdstringnoDefaults to your project; must match the instance's project

Each entry takes the fields of a batch-graph task (name is required; durationMinutes, durationSeconds, earliestStart, deadline, isFixed, bufferAfterMinutes, assigneePlatformUserId, tags, priority, subjectType, subjectId, metadata, description, requirements), with these differences:

  • ref is optional. It is only a key for the refs map in the response.
  • resourceRefs is not accepted (there are no same-request resources here), and a requirement may not use resourceRef: give it a resourceId or poolId.
  • The body is strict: an unknown field is a 400 VALIDATION_ERROR, never silently dropped.

Returns

Promise<ApiResponse<{ tasks, refs, schedule }>>

FieldDescription
tasksThe created task rows, in the order you sent them
refsref to task id, for the tasks that had a ref
schedule{ requiredStart, makespanMinutes } summary of the batch

Ids are handed out in ascending order of the tasks array, so equal planner ties always break in the order you listed the tasks. The response does not contain conflicts. If a requirement over-allocates a resource, the task is created and the conflict is stored: read it with conflicts.list() (see Conflicts).

Atomicity and errors

The batch is all-or-nothing. Everything is validated before anything is written, and a rejected batch creates no tasks.

StatuscodeCause
400VALIDATION_ERROREmpty batch, more than 500 tasks, unknown field, bad requirement shape
404NOT_FOUNDUnknown workflowInstanceId
422DUPLICATE_REFThe same ref twice (refs lists them)
422REQUIREMENT_EXCEEDS_CAPACITYA claim needs more units than the resource has
422UNKNOWN_REFA requirement names a resource that does not exist
422STATE_NOT_SUPPORTEDA requirement gives state for a resource that declares none
typescript
// A press with 2 slots. Both press runs need both slots.
const { data: press } = await client.resources.create({ name: `Press ${suffix}`, type: 'asset', capacity: 2 })
 
// ── 1. Create several tasks in one request ──────────────────────────────────────
const start = new Date(Date.now() + 60 * 60_000).toISOString()
const { data } = await client.tasks.createBatch({
  tasks: [
    { ref: 'plates', name: `Make plates ${suffix}`, durationMinutes: 30, earliestStart: start },
    { ref: 'run', name: `Print run ${suffix}`, durationMinutes: 90, requirements: [{ resourceId: press.id, units: 2 }] },
    { name: `Pack boxes ${suffix}`, durationMinutes: 20 }, // no ref: not in `refs`
  ],
})
 
// ids are handed out in ascending order of the tasks array, and `refs` maps ref -> id
if (data.tasks.length !== 3) throw new Error('expected 3 tasks')
const ids = data.tasks.map((t) => t.id)
if (JSON.stringify(ids) !== JSON.stringify([...ids].sort())) throw new Error('ids are not ascending in input order')
if (data.refs.plates !== ids[0] || data.refs.run !== ids[1]) throw new Error('refs do not match input order')
if (Object.keys(data.refs).length !== 2) throw new Error('only tasks with a ref are in refs')

Runnable: docs-examples/tasks-batch.mts. Batch tasks are planned together as their own group: they are not linked to existing tasks unless they share an assignee (those are re-planned together with the assignee's other work). Add dependencies afterwards with the Dependencies API.

Resource requirements: tasks.setRequirements / tasks.listRequirements

A requirement is a claim on a resource: how many units of it the task holds, and when within the task it holds them. The planner checks capacity against the occupied time of the claims, so a resource a task only touches at the start and end is free in between. This is a different thing from capability requirements on templates ("some resource that can do X") and from assignResource (compared below). The Resource Requirements guide explains the model; this is the reference.

setRequirements(taskId, requirements) replaces the task's whole set (it does not merge), then recomputes. Send [] to remove every claim. listRequirements(taskId) returns the stored claims. The replace is atomic: a rejected call leaves the previous set untouched. Rows get new ids on every setRequirements call.

Requirement fields

FieldTypeDefaultDescription
resourceIdstring (uuid)A specific resource. Exactly one of resourceId / poolId
poolIdstringA pool: capability:<capabilityId> (capacity = sum of its active resources, a resource with no capacity counts as 1) or any other string, which is opaque and unconstrained. With an assignmentPolicy on the job, a capability: pool requirement is resolved to one member (Assign from a pool)
unitsinteger > 01How many units are held. 0, negatives and fractions are a 400
occupancyobject{ mode: 'full' }When within the task the units are held (below)
statenumbernoneThe shared-state value the claim needs. Only valid on a single resource that declares stateKey / stateTolerance; see Resources

Limits: at most 50 requirements per task (400 above that). The body is strict, so unknown fields are a 400. Inside a batch graph a requirement may use resourceRef instead of resourceId; setRequirements may not.

Occupancy modes

occupancyFieldsHolds the resource
{ mode: 'full' }nonefor the whole task
{ mode: 'bookends', startMinutes, endMinutes }both required, integers >= 0for the first startMinutes and the last endMinutes of the task only
{ mode: 'none' }nonenever. The claim is stored and returned, but takes no capacity

Bookends are measured from the task's start and end and are not checked against the duration: startMinutes + endMinutes may exceed it. The two windows are clamped to the task and merged where they overlap, so a 60-minute task with 45 + 45 holds the resource for the whole 60 minutes, once (not twice). 0 + 0 holds nothing. Extra keys inside occupancy are a 400.

Validation

StatuscodeCause
400VALIDATION_ERRORBad shape (both or neither of resourceId/poolId, units not a positive integer, a bad occupancy, more than 50 requirements, unknown field)
404NOT_FOUNDUnknown task
422UNKNOWN_REFresourceId does not exist
422REQUIREMENT_EXCEEDS_CAPACITYOne claim asks for more units than the resource (or pool) can ever provide. A capability: pool with no resources has capacity 0. refs is [taskName, resourceId], and units and capacity are included
422STATE_NOT_SUPPORTEDstate on a pool or on a resource that declares no stateKey

Capacity is checked per claim, not summed. Two claims of 2 units on a capacity-2 resource in the same task are accepted; the planner then reports the overload as a conflict. A resource with no capacity is unconstrained: any units is accepted and never conflicts.

Example

A press with two slots and one operator. The press run needs both slots; the operator is only needed to load and unload.

typescript
// 60-minute press run holding both slots. The operator is needed for the first and last 10 minutes.
// Two helper tasks need the operator the whole time they run.
const { data: batch } = await client.tasks.createBatch({
  tasks: [
    { ref: 'run', name: `Press run ${suffix}`, durationMinutes: 60, earliestStart: at(0),
      requirements: [
        { resourceId: press.id, units: 2 },
        { resourceId: operator.id, occupancy: { mode: 'bookends', startMinutes: 10, endMinutes: 10 } },
      ] },
    // minutes 20-40: inside the free middle of the run, so no clash
    { ref: 'proof', name: `Proof sheet ${suffix}`, durationMinutes: 20, earliestStart: at(20),
      requirements: [{ resourceId: operator.id }] },
    // minutes 5-20: overlaps the operator's first 10 minutes of the run
    { ref: 'early', name: `Early check ${suffix}`, durationMinutes: 15, earliestStart: at(5),
      requirements: [{ resourceId: operator.id }] },
  ],
})
const ids = batch.refs as Record<string, string>

Replace the set, clear it, and read it back:

typescript
// ── setRequirements REPLACES the whole set ───────────────────────────────────────────
// Make the operator "none" on the run (recorded, but never holds the operator), keep the press.
const { data: replaced } = await client.tasks.setRequirements(ids.run!, [
  { resourceId: press.id, units: 2 },
  { resourceId: operator.id, occupancy: { mode: 'none' } },
])
 
// An empty array removes every claim
const { data: cleared } = await client.tasks.setRequirements(ids.run!, [])
if (cleared.length !== 0 || (await client.tasks.listRequirements(ids.run!)).data.length !== 0) throw new Error('not cleared')

Runnable: docs-examples/tasks-requirements.mts (it also exercises every validation error above).

What happens to the schedule

Requirements do not move tasks by themselves. When two claims on a resource overlap in time and together exceed its capacity, the plan keeps them where they are and stores a resource_capacity_breach conflict on the claim that started last. To have the planner delay tasks until the resource is free, create the job with resourceLeveling: 'delay' (Resource Leveling). Other jobs that claim the same resource count toward usage.

setRequirements vs assignResource vs resources.allocate

setRequirementsassignResourceresources.allocate
Recordsunits + time within the taskthe resource id in task.resourceIdsan allocation row (allocated / released)
Checks capacityyes: units against capacity at creation, overlap on the planno. Accepts duplicates and ids that do not existyes: 409 CAPACITY_EXCEEDED if the resource is full while the task's window overlaps other allocations
Understands units and bookendsyesnono (one allocation = one slot, for the task's whole window)
Recomputes the planyesnono
Reports overload as a conflictyes (resource_capacity_breach)noonly through the older whole-task capacity pass, and not for tasks that have requirements

Use setRequirements (or requirements when creating) for anything time-based or multi-unit: it is the only one the planner and leveling use for capacity. Use assignResource as a plain label ("this task uses this press") and resources.allocate when you want a capacity-checked booking of a whole task window. An allocation is not a requirement: allocating a resource to a task does not stop a requirement claim from using the same capacity. See Resources.

Known limitations

  • A requirements change recomputes only the job you edited. A clash that was stored against a task in another job sharing the resource is not cleared (or created) until that job recomputes, for example when you re-save its requirements. docs-examples/tasks-requirements.mts does this.
  • createBatch returns no conflicts (above).
  • Pool assignment ("any of these helpers") needs an assignmentPolicy on the job (set through createWithGraph). Without one a capability: pool is only a total capacity. setRequirements keeps the assigned member for a requirement that stays on the same pool; membership changes take effect at the next recompute.

tasks.get(taskId)

Fetches a single task including engine-computed fields (slackSec, isCritical).

typescript
const { data: task } = await client.tasks.get('tsk_abc123')
 
console.log(task.slackSec)   // seconds before the critical-path deadline
console.log(task.isCritical) // true if on the critical path

tasks.list(opts?)

Options

OptionTypeDescription
workflowInstanceIdstringFilter to one instance
statusTaskStatusFilter by status
typescript
const { data: tasks } = await client.tasks.list({
  workflowInstanceId: 'wi_abc123',
  status: 'ready',
})

The list returns the tasks you can see (as assignee, creator or owner; every task of the project for a project-bound key), ordered by deadline (tasks without one last), then earliestStart, then newest first. Cancelled tasks are never listed, not even with status: 'cancelled'. A status value that is not one of the seven statuses is ignored rather than rejected. A guest token sees only the tasks in its scope.

tasks.update(taskId, input)

Updates any mutable task field. All fields are optional.

typescript
await client.tasks.update('tsk_abc123', {
  name: 'Review final contract',
  durationMinutes: 45,
  deadline: '2026-07-02T12:00:00Z',
  priority: 'high',
})

status can be set here (pending, ready, running, blocked, cancelled, completed, failed). deadline: null and priority: null clear them. Reassigning goes through tasks.assign. In a job scheduled by the planner (schedulingDirection: 'backward' or resourceLeveling: 'delay'), moving a task to running pins it where it started, so later recomputes never move it. Errors: 400 VALIDATION_ERROR, 404 NOT_FOUND, 403 FORBIDDEN (a task you cannot see).

tasks.complete(taskId)

Marks a task as completed. Downstream tasks that were waiting on this one become ready and are returned in unlockedTasks.

typescript
const { data } = await client.tasks.complete('tsk_abc123')
 
console.log(data.task.status) // 'completed'
console.log(data.unlockedTasks) // [{ id: '...', name: 'Send confirmation email' }]
StatuscodeCause
404NOT_FOUNDUnknown task
403FORBIDDENA task you cannot see (a guest token is checked against its scope instead)
400ALREADY_DONEThe task is already completed
409REQUIRED_ARTIFACTS_MISSINGThe task has required artifacts without an approved approval; details lists them

Completing a task also emits task.completed and, for each successor that becomes ready, task.ready.

tasks.assign(taskId, userId)

Assigns a task to a platform user (user must exist in your Clockwork account).

typescript
await client.tasks.assign('tsk_abc123', 'user_xyz')

tasks.assignResource(taskId, resourceId)

Adds a resource id to the task's resourceIds. It is a label: it does not check that the resource exists or has capacity, accepts the same id twice, does not recompute the schedule, and does not create conflicts. For capacity use setRequirements or resources.allocate (compare them above). See Resources.

typescript
await client.tasks.assignResource('tsk_abc123', 'res_xyz')

tasks.delay(taskId, input)

Delays a task. The slip propagates automatically to all downstream tasks.

Parameters

ParameterTypeDescription
delayMinutesinteger > 0Push the task's earliestStart back by this many minutes
newEarliestStartstringISO 8601 — set an absolute new earliestStart (it wins if both are sent)
reasonstringOptional note, stored on the task (metadata.delayReason) and carried by the task.delayed event

Send one of delayMinutes / newEarliestStart: neither is 400 VALIDATION_ERROR (Provide delayMinutes or newEarliestStart), and so is a non-positive or fractional delayMinutes. The delay moves the task's lower bound: it recomputes the job, emits task.delayed and, if other tasks moved, one schedule.changed. 404 NOT_FOUND for an unknown task, 403 FORBIDDEN for one you cannot see. A guest token can only delay, never move a task earlier (422).

typescript
await client.tasks.delay('tsk_abc123', {
  delayMinutes: 60,
  reason: 'Waiting on legal review',
})

tasks.delete(taskId)

Permanently deletes a task. Dependent tasks are disconnected and rescheduled.

typescript
await client.tasks.delete('tsk_abc123')

Schedule fields on a task

FieldMeaning
earliestStartLower bound on when the task may start. You set it; a delay raises it. On a forward job with leveling off (the default) the engine also writes the live scheduled start here after every recompute, so it moves when upstream tasks slip. On a backward or leveled job it stays your lower bound and is never overwritten by planning
plannedStart / plannedEndWhere the last whole-job plan placed the task. null for a plain tasks.create
latestStartThe latest start that still lets the job meet its deadline or anchor
slackSecEngine-computed float, in seconds (latestStart - earliestStart on an anchored job); stored as a 32-bit integer, so it is capped at about 68 years
isCriticalWhether the task is on the critical path

Which of these stay current. plannedStart, plannedEnd, latestStart and the job's requiredStart / makespanMinutes are rewritten on every recompute only for jobs the planner manages (schedulingDirection: 'backward' or resourceLeveling: 'delay'). On a forward job with leveling off they keep the values from creation after a delay (checked: delaying an upstream task by 120 minutes moved earliestStart 120 minutes and left plannedStart untouched). Read earliestStart there.

Anchor windows. A task in createWithGraph may carry anchorWindow: { minOffsetMinutes?, maxOffsetMinutes?, applyTo?: 'finish' | 'start' } (minutes from the job's anchor, negative = before). It is not a field of Task: it creates an anchor_window constraint attached to the task. Requirements are likewise not part of Task; read them with tasks.listRequirements.

Task type reference

typescript
type TaskStatus = 'pending' | 'ready' | 'running' | 'blocked' | 'cancelled' | 'completed' | 'failed'
type TaskPriority = 'high' | 'medium' | 'low'
 
interface Task {
  id: string
  name: string
  description: string | null
  status: TaskStatus
  priority: TaskPriority | null
  earliestStart: string        // ISO 8601. Lower bound on the start; on a forward job with leveling off it is also the live scheduled start
  deadline: string | null
  durationMinutes: number
  durationSeconds: number | null
  bufferAfterMinutes: number
  bufferAfterSeconds: number | null
  isFixed: boolean
  slackSec: number | null      // Engine-computed: float before deadline breach
  isCritical: boolean | null   // Engine-computed: on the critical path?
  plannedStart: string | null  // Where the last whole-job plan placed the task. null for a plain tasks.create; only kept current on planner-managed jobs
  plannedEnd: string | null    // plannedStart + duration
  latestStart: string | null   // Latest start that still meets the deadline / anchor
  workflowInstanceId: string | null
  assigneePlatformUserId: string | null
  resourceIds: string[]        // ids added with assignResource (a label; not a capacity claim)
  tags: string[]
  subjectType: string | null
  subjectId: string | null
  createdAt: string
  updatedAt: string
}

Resource claims are not part of Task: read them with tasks.listRequirements(taskId):

typescript
type RequirementOccupancy =
  | { mode: 'full' }
  | { mode: 'bookends'; startMinutes: number; endMinutes: number }
  | { mode: 'none' }
 
interface RequirementInput {
  resourceId?: string
  poolId?: string
  units?: number                 // default 1
  occupancy?: RequirementOccupancy // default { mode: 'full' }
  state?: number
}
 
interface TaskResourceRequirement {
  id: string
  taskId: string
  resourceId: string | null
  poolId: string | null
  units: number
  occupancy: RequirementOccupancy
  state?: number | null
  assignedResourceId?: string      // the pool member chosen by an assignmentPolicy; absent otherwise
}

The task rows returned by the API carry a few more fields than Task declares (for example metadata).