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
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.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
| Method | Description |
|---|---|
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 / listCapabilities | Which 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
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Display name |
resourceTypeId | string (uuid) | one of the two | The resource's type (create types with client.resourceTypes). |
type | 'human' | 'agent' | 'service' | 'asset' | one of the two | Deprecated 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 |
capacity | positive integer | no | Units 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) |
availability | unknown[] | no | Absolute windows { from, to } or { start, end } (ISO 8601); see Availability windows |
stateKey | string | no | Name of the shared state, e.g. 'temperature_c'. Goes together with stateTolerance |
stateTolerance | number >= 0 | no | How far apart two overlapping claims' state values may be |
stateSetupMinutes | integer >= 0 | no | Changeover time between claims further apart than the tolerance. Needs stateKey |
projectId / organizationId | string | no | Default to your personal project and its organization |
status | 'active' | 'unavailable' | 'retired' | no | Default 'active'. Only active resources count toward a pool and can be assigned |
description, properties, externalId, platformUserId, costRatePerHour | no | Descriptive fields (costRatePerHour is a non-negative integer) | |
metadata | Record<string, unknown> | no | Arbitrary data |
Example
// 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).
| Claim | Occupied intervals (60-minute task starting at 0) | Units in use |
|---|---|---|
units: 1, full | 0-60 | 1 |
units: 2, full (a job that needs both slots) | 0-60 | 2 (nothing else fits) |
units: 1, bookends 10 / 10 (an operator who only loads and unloads) | 0-10 and 50-60 | 1 during those stretches, 0 in between |
units: 1, none | none | 0 |
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).
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 to | Use |
|---|---|
| Say how many things the resource can do at once | capacity on the resource |
| Say a task needs N units, for all or part of its time, and have the planner check or resolve it | A task requirement: requirements on create, or tasks.setRequirements |
| Resolve an over-allocation automatically | resourceLeveling: 'delay' (Resource Leveling) |
| Book a resource for a task's whole window with a capacity check, without units or bookends | resources.allocate (below) |
| Just record which resource a task uses | tasks.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.
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.
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.
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 againEditing 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:
stateKeynames the state (a label),stateToleranceis how far apart two overlapping claims may be,stateSetupMinutesis the changeover needed between claims further apart than that.stateKeyandstateTolerancego together (400otherwise);stateSetupMinutesneedsstateKey.- A claim with no
stateis compatible with everything.stateis only valid on a single resource that declares a state (422 STATE_NOT_SUPPORTEDon a pool or on a stateless resource). - On
update, sendingstateKeyandstateTolerancealso resetsstateSetupMinutestonullunless you send it again. Sendnullfor all three to clear the state. - Details, and how conflicts and leveling treat it: Resource Requirements.
// ── 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.
// ── 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 ofupdateto 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: callworkflowInstances.recomputeto 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
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
}Related
- Tasks — claim units of a resource with
tasks.setRequirements();tasks.assignResource()only labels - Resource Requirements and Resource Leveling — the guides
- Capability Pools — group interchangeable resources and let the planner pick one
- Roles — fill template role slots with resources
- Conflicts — resource contention surfaces as a conflict