Resources

A Resource is anything that executes work — a human, an AI agent, an external service, or a physical asset. Resources have an optional capacity (how many units can be in use at the same moment). Clockwork checks it when you allocate, and the planner checks it over time against the requirements tasks place on the resource.

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.resources

  • Registering your AI agents so they can be assigned to tasks and tracked
  • Modeling human specialists with limited availability (capacity = 1 means one task at a time)
  • Tracking shared assets (conference rooms, lab equipment) with capacity constraints
  • Filling role slots in a workflow template (see Roles)

Methods

MethodDescription
create(input)Register a new resource
get(resourceId)Fetch a resource
update(resourceId, input, opts?)Change capacity, availability, shared state, status and more; replans affected planner-managed jobs in the background (opts.recompute: false to skip)
list(opts?)List resources in a project
allocate(resourceId, input)Allocate to an execution, task, or actor
release(resourceId, allocationId?)Release an allocation
assign(resourceId, taskId)Alias for allocate(resourceId, { taskId })
listAllocations(resourceId, opts?) / listProjectAllocations(opts?)Allocations of one resource, or of a whole project (status: 'allocated' | 'released')
updateAvailability(resourceId, availability)Alias for update(resourceId, { availability })
assignCapability / removeCapability / listCapabilitiesWhich capabilities the resource holds; the membership of a capability pool
findMatching(input) / explain(resourceId, taskId)Rank resources by capability, availability and cost; explain why one fits a task

resources.create(input)

Parameters

ParameterTypeRequiredDescription
namestringyesDisplay name
resourceTypeIdstring (uuid)one of the twoThe resource's type (create types with client.resourceTypes).
type'human' | 'agent' | 'service' | 'asset'one of the twoDeprecated tag, stored on the resource and, when the project belongs to an organization, resolved to that organization's matching resourceTypeId if you omit it. Sending neither is 400 VALIDATION_ERROR
capacitypositive integernoUnits that can be in use at once (unbounded if omitted: a resource with no capacity never conflicts; inside a capability pool it counts as 1)
availabilityunknown[]noAbsolute windows { from, to } or { start, end } (ISO 8601); see Availability windows
stateKeystringnoName of the shared state, e.g. 'temperature_c'. Goes together with stateTolerance
stateTolerancenumber >= 0noHow far apart two overlapping claims' state values may be
stateSetupMinutesinteger >= 0noChangeover time between claims further apart than the tolerance. Needs stateKey
projectId / organizationIdstringnoDefault to your personal project and its organization
status'active' | 'unavailable' | 'retired'noDefault 'active'. Only active resources count toward a pool and can be assigned
description, properties, externalId, platformUserId, costRatePerHournoDescriptive fields (costRatePerHour is a non-negative integer)
metadataRecord<string, unknown>noArbitrary data

Example

typescript
// Register an AI agent with capacity for 3 concurrent tasks
const { data: agent } = await client.resources.create({
  name: 'Claude Analysis Agent',
  type: 'agent',
  capacity: 3,
  metadata: { model: 'claude-opus-4-8', endpoint: 'https://api.example.com/agent' },
})
 
// Register a human specialist (one task at a time)
const { data: specialist } = await client.resources.create({
  name: 'Alice Chen — Legal',
  type: 'human',
  capacity: 1,
})

Capacity, requirements and occupancy

capacity is how many units of the resource can be in use at the same instant. A task claims units with a requirement (units, default 1) and says when within the task it holds them (occupancy: full, bookends or none). The planner turns each requirement into occupied intervals and checks, at every moment, that the units of all occupied intervals on the resource add up to at most capacity.

Example: a press with capacity: 2 (two slots).

ClaimOccupied intervals (60-minute task starting at 0)Units in use
units: 1, full0-601
units: 2, full (a job that needs both slots)0-602 (nothing else fits)
units: 1, bookends 10 / 10 (an operator who only loads and unloads)0-10 and 50-601 during those stretches, 0 in between
units: 1, nonenone0

So two runs that each need 1 slot can run side by side (1 + 1 = 2). Two runs that each need both slots cannot (2 + 2 = 4 > 2): the planner reports a resource_capacity_breach conflict, or delays one run if the job opted into resourceLeveling: 'delay'. An operator with capacity: 1 and bookends is free in the middle of a run, so another task can use them there. A resource with no capacity is never over-allocated. A claim for more units than capacity is rejected when you create it (422 REQUIREMENT_EXCEEDS_CAPACITY).

typescript
const start = new Date(Math.ceil((Date.now() + 2 * 3_600_000) / 60_000) * 60_000).toISOString()
 
/** Two independent 60-minute press runs that can both start at `start`. */
function plan(unitsPerRun: number, resourceLeveling: 'off' | 'delay') {
  return client.workflowInstances.createWithGraph({
    instance: { name: `Press plan ${suffix} ${unitsPerRun}/${resourceLeveling}`, resourceLeveling },
    resources: [{ ref: 'press', name: `Press ${suffix}`, type: 'asset', capacity: 2 }], // 2 slots
    tasks: ['run-a', 'run-b'].map((ref) => ({
      ref, name: ref, durationMinutes: 60, earliestStart: start,
      requirements: [{ resourceRef: 'press', units: unitsPerRun }],
    })),
  })
}
const peak = (r: Awaited<ReturnType<typeof plan>>) => r.data.schedule.utilization.find((u) => u.resourceRef === 'press')!.peakUnits
const startOf = (r: Awaited<ReturnType<typeof plan>>, ref: string) => r.data.schedule.tasks.find((t) => t.ref === ref)!.plannedStart
 
// 1 slot each: both runs fit side by side (1 + 1 <= 2)
const oneEach = await plan(1, 'off')
if (peak(oneEach) !== 2 || oneEach.data.schedule.conflicts.length !== 0) throw new Error('1+1 on 2 slots should fit')
if (startOf(oneEach, 'run-a') !== startOf(oneEach, 'run-b')) throw new Error('both should start together')
 
// both slots each, leveling off: the planner keeps both at the same time and REPORTS the overload
const reported = await plan(2, 'off')
if (peak(reported) !== 4) throw new Error('expected 4 units in use on a 2-slot press')
const breach = reported.data.schedule.conflicts
if (breach.length !== 1 || breach[0]!.type !== 'resource_capacity_breach') throw new Error('expected one breach: ' + JSON.stringify(breach))
console.log('off  :', breach[0]!.description)
 
// both slots each, leveling delay: one run waits for the other
const leveled = await plan(2, 'delay')
if (peak(leveled) !== 2 || leveled.data.schedule.conflicts.length !== 0) throw new Error('delay should resolve it')
const a = new Date(startOf(leveled, 'run-a')).getTime()
const b = new Date(startOf(leveled, 'run-b')).getTime()
if (Math.abs(a - b) !== 60 * 60_000) throw new Error('the second run should start 60 minutes after the first')
console.log('delay: second run starts', Math.abs(a - b) / 60_000, 'minutes after the first')

Runnable: docs-examples/resources-capacity.mts. schedule.utilization reports, per capacity-limited resource, peakUnits and occupied vs available unit-minutes.

Which API to use

You want toUse
Say how many things the resource can do at oncecapacity on the resource
Say a task needs N units, for all or part of its time, and have the planner check or resolve itA task requirement: requirements on create, or tasks.setRequirements
Resolve an over-allocation automaticallyresourceLeveling: 'delay' (Resource Leveling)
Book a resource for a task's whole window with a capacity check, without units or bookendsresources.allocate (below)
Just record which resource a task usestasks.assignResource (a label only, nothing is checked)

resources.allocate(resourceId, input)

Allocates a resource to a task or execution. It counts the resource's open allocations whose task windows overlap this task's window (earliest start to end, including buffer) and throws 409 CAPACITY_EXCEEDED when that count has reached capacity; a task later in time can still be allocated. An allocation with no taskId is treated as overlapping everything. Passing capabilityId also requires the resource to hold that capability (409 MISSING_CAPABILITY). Allocation counts one unit per call and ignores requirements: it does not see units or bookends, and a requirement claim does not block an allocation (or the reverse). Prefer requirements when you need either.

typescript
const { data: allocation } = await client.resources.allocate('res_abc123', {
  taskId: 'tsk_xyz',
})
 
console.log(allocation.id, allocation.status) // alloc_... 'allocated'

resources.release(resourceId, allocationId?)

Releases an allocation. If allocationId is omitted, releases the most recent open allocation.

typescript
await client.resources.release('res_abc123', 'alloc_xyz')

resources.update(resourceId, input)

A partial update: only the fields you send change. capacity: null removes the limit; availability: [] removes every window; status: 'retired' takes the resource out of every pool. stateKey, stateTolerance and stateSetupMinutes follow the rules under Shared state. With planning v2 on, an unusable availability window is a 400 here too.

typescript
await client.resources.update(press.id, { capacity: 3 })           // the press now has 3 slots
await client.resources.update(press.id, { capacity: null })        // no limit
await client.resources.update(operator.id, { status: 'unavailable' }) // leaves pools until active again

Editing a resource does not recompute plans. The next mutation in an affected job (a delay, a completion, a requirements change) does. See Known limitations.

Shared state

Some resources can be shared only while in a compatible state: a drying rack that holds a 60 degree job and a 65 degree job together, but not a 90 degree one. Declare it on the resource and give each requirement the state value it needs:

  • stateKey names the state (a label), stateTolerance is how far apart two overlapping claims may be, stateSetupMinutes is the changeover needed between claims further apart than that. stateKey and stateTolerance go together (400 otherwise); stateSetupMinutes needs stateKey.
  • A claim with no state is compatible with everything. state is only valid on a single resource that declares a state (422 STATE_NOT_SUPPORTED on a pool or on a stateless resource).
  • On update, sending stateKey and stateTolerance also resets stateSetupMinutes to null unless you send it again. Send null for all three to clear the state.
  • Details, and how conflicts and leveling treat it: Resource Requirements.
typescript
// ── Shared state: a dryer that two jobs may share only at similar temperatures ───────
const { data: dryer } = await client.resources.create({
  name: `Dryer ${suffix}`, type: 'asset', capacity: 2,
  stateKey: 'temperature_c', stateTolerance: 10, stateSetupMinutes: 20,
})
if (dryer.stateKey !== 'temperature_c' || dryer.stateTolerance !== 10 || dryer.stateSetupMinutes !== 20) throw new Error('state not stored')
 
await expectError('stateKey without stateTolerance', 400, () =>
  client.resources.create({ name: `Bad ${suffix}`, type: 'asset', stateKey: 'temperature_c' }))
await expectError('stateSetupMinutes without stateKey', 400, () =>
  client.resources.create({ name: `Bad ${suffix}`, type: 'asset', stateSetupMinutes: 5 }))
 
// Update: key and tolerance go together; an update that sets them also resets the setup time
// unless you send it again.
const { data: updated } = await client.resources.update(dryer.id, { stateKey: 'temperature_c', stateTolerance: 15 })
if (updated.stateTolerance !== 15 || updated.stateSetupMinutes !== null) throw new Error('expected setup minutes to be reset by this update')
// Clear all three with null
const { data: cleared } = await client.resources.update(dryer.id, { stateKey: null, stateTolerance: null, stateSetupMinutes: null })
if (cleared.stateKey !== null || cleared.stateTolerance !== null) throw new Error('state not cleared')

Availability windows

availability is an array. Entries shaped { from, to } or { start, end } with ISO 8601 timestamps are absolute windows: the resource exists only inside their union, and the planner requires every occupied stretch of every claim to fall inside it (a bookends claim may start before the window if only the attended stretches are inside). No window entries means always available. With planning v2 on, an entry that looks like a window but is unusable (not a timestamp, or ending before it starts) is a 400 on create and update, so a typo is not silently "always available". Other shapes, such as the weekly daysOfWeek / startTime / endTime, are stored but not interpreted by the planner.

With resourceLeveling: 'delay' the planner moves a task to the next window that can hold it. With leveling off, occupied time outside the windows is reported as a resource_capacity_breach conflict ("outside its availability"). A resource with windows but no capacity is constrained only by its windows.

typescript
// ── Absolute availability windows ────────────────────────────────────────────────────
// An operator who is only on site 11:00-20:00 UTC, three days from now.
const day = new Date()
day.setUTCDate(day.getUTCDate() + 3)
const at = (h: number) => new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate(), h)).toISOString()
const windows = [{ from: at(11), to: at(20) }] // { start, end } is accepted too
 
const { data: operator } = await client.resources.create({ name: `Operator ${suffix}`, type: 'human', capacity: 1, availability: windows })
if (!Array.isArray(operator.availability) || operator.availability.length !== 1) throw new Error('availability not stored')
 
// Unusable window entries are a 400 (while planning v2 is on), not "always available"
await expectError('window ends before it starts', 400, () =>
  client.resources.create({ name: `Bad ${suffix}`, type: 'human', availability: [{ from: at(20), to: at(11) }] }))
await expectError('window is not a date', 400, () =>
  client.resources.create({ name: `Bad ${suffix}`, type: 'human', availability: [{ from: 'noon', to: 'evening' }] }))
await expectError('update with an unusable window', 400, () => client.resources.update(operator.id, { availability: [{ from: 'x', to: 'y' }] }))
 
// A 60-minute job that wants to start at 09:00, before the operator arrives
const job = (resourceLeveling: 'off' | 'delay') =>
  client.workflowInstances.createWithGraph({
    instance: { name: `Window plan ${suffix} ${resourceLeveling}`, resourceLeveling },
    resources: [{ ref: 'operator', name: `Operator ${suffix} ${resourceLeveling}`, type: 'human', capacity: 1, availability: windows }],
    tasks: [{ ref: 'setup', name: 'Set up the press', durationMinutes: 60, earliestStart: at(9), requirements: [{ resourceRef: 'operator' }] }],
  })
 
const off = await job('off')
const breach = off.data.schedule.conflicts
if (breach.length !== 1 || breach[0]!.type !== 'resource_capacity_breach' || !breach[0]!.description.includes('outside its availability')) {
  throw new Error('expected an availability breach: ' + JSON.stringify(breach))
}
console.log('off  :', breach[0]!.description)
 
const delayed = await job('delay')
const placed = delayed.data.schedule.tasks[0]!
if (delayed.data.schedule.conflicts.length !== 0 || placed.plannedStart !== at(11)) throw new Error('expected the task to wait for the window: ' + placed.plannedStart)
console.log('delay: starts at', placed.plannedStart)

Runnable: docs-examples/resources-state-availability.mts.

Known limitations

  • With planning v2 on, editing a resource's capacity, windows, status or shared state replans the planner-managed jobs that use it, after the response and in the background, so the new plan can lag by a few seconds. At most 25 jobs are replanned per edit (most recently updated first); a job already replanned since the edit is skipped; repeated edits coalesce. The response then carries recompute: { scheduled, skipped, capped }; pass { recompute: false } as the third argument of update to opt out. Jobs that are not planner-managed (no backward anchor, no leveling) are not touched. Where the host cannot keep work running after the response, the replan is best effort and can be cut short: call workflowInstances.recompute to make sure. A failed background replan never fails the edit.
  • Windows need not be sorted or disjoint (they are merged), but a task longer than every window never fits: you get the conflict, not an error.

Type reference

typescript
type ResourceStatus = 'active' | 'unavailable' | 'retired'
 
interface Resource {
  id: string
  projectId: string | null
  organizationId?: string | null
  name: string
  type?: 'human' | 'agent' | 'service' | 'asset' | null   // deprecated tag
  resourceTypeId?: string | null
  description?: string | null
  capacity?: number | null
  availability?: unknown[] | null   // { from, to } / { start, end } windows
  properties?: Record<string, unknown> | null
  metadata?: Record<string, unknown> | null
  status?: ResourceStatus
  externalId?: string | null
  platformUserId?: string | null
  costRatePerHour?: number | null
  stateKey?: string | null
  stateTolerance?: number | null
  stateSetupMinutes?: number | null
  createdAt: string
}
 
interface ResourceAllocation {
  id: string
  resourceId: string
  executionId: string | null
  taskId: string | null
  actorId: string | null
  status: 'allocated' | 'released'
  allocatedAt: string
  releasedAt: string | null
}