Workflow Instances

A Workflow Instance is a live execution of a workflow — a collection of tasks running toward completion. Instances can be created from a template (inheriting its structure) or built up programmatically by adding tasks and dependencies directly.

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. Template overrides, excludes and role bindings (F8) are merged (#275, 2026-10-12) but not yet deployed to production: the production deployment of that merge failed on Vercel and is awaiting a retry. The rest of this page is live.

When to use client.workflowInstances

  • Launching a new workflow when a business event occurs (deal signed, customer created, order placed)
  • Monitoring overall workflow status (running, blocked, completed)
  • Filtering instances by subject to find all workflows associated with a specific customer or project
  • Cancelling a workflow when the underlying business event is voided

Methods

MethodDescription
create(input)Launch a new instance (optionally from a template, with overrides, excludes and role bindings)
createWithGraph(input)Create a job with its resources, tasks and dependencies in one atomic request, with an optional anchor, direction and leveling
get(instanceId)Fetch instance and current status
cancel(instanceId)Cancel instance and all open tasks
recompute(instanceId)Replan one planner-managed job now and return the fresh schedule summary
list(opts?)List instances filtered by project, status, or subject

workflowInstances.create(input)

Parameters

ParameterTypeRequiredDescription
templateIdstringnoInstantiate from a published template
projectIdstringnoDefaults to personal project
namestringnoDisplay name
subjectTypestringnoOpaque label for the triggering entity (e.g., 'customer')
subjectIdstringnoID of the triggering entity
jobValuenumbernoWhole-number value of the job
metadataRecord<string, unknown>noArbitrary key-value data
anchorDatestring (ISO 8601)noWith templateId: the date each template node's offsetFromAnchorMin is measured from (default now). Not the finish anchor below
anchor{ type: 'finish'; at: string }noRequired finish of the whole plan. Behind CLOCKWORK_PLANNING_V2
schedulingDirection'forward' | 'backward'noDefault 'forward'. 'backward' requires anchor (otherwise 422 ANCHOR_REQUIRED). Behind CLOCKWORK_PLANNING_V2
resourceLeveling'off' | 'delay'noDefault 'off'. Behind CLOCKWORK_PLANNING_V2
overrides, exclude, excludeMode, roleBindingsnoCustomise a template instantiation; need templateId. Behind CLOCKWORK_PLANNING_V2. See Instantiate a template with overrides

anchor, schedulingDirection and resourceLeveling on create

Behind CLOCKWORK_PLANNING_V2, create stores these three fields on the job with exactly the validation and meaning they have in createWithGraph: anchor.type must be 'finish', anchor.at an ISO 8601 UTC timestamp, and schedulingDirection: 'backward' without an anchor is a 422 ANCHOR_REQUIRED. A job created empty has nothing to plan yet (requiredStart and makespanMinutes stay null); every task you add afterwards is planned against the stored anchor, direction and leveling.

With the flag off the platform behaves as it always did: these fields (and the template options below) are silently dropped. The call returns 201 and the instance comes back with anchor: null, schedulingDirection: 'forward' and resourceLeveling: 'off'. Check the returned instance if your code may talk to a platform without the flag.

The request schema is deliberately not strict: any other unknown field is still ignored without an error, as before, so existing clients that send extra fields keep working. One exception, flag on: assignmentPolicy, resources, tasks and dependencies cannot be honored by create, so they answer 400 UNSUPPORTED_FIELD (the message names the field and points to createWithGraph) instead of being dropped. PATCH (update) answers the same for anchor, schedulingDirection, resourceLeveling and those four. Flag off: ignored, as before. (createWithGraph is strict and answers 400 for an unknown field.)

Example

typescript
// Create from a template
const { data: instance } = await client.workflowInstances.create({
  templateId: 'tmpl_abc123',
  subjectType: 'customer',
  subjectId: 'cust_xyz',
})
 
console.log(instance.id, instance.status) // a UUID, 'running'
typescript
// Create standalone (no template)
const { data: instance } = await client.workflowInstances.create({
  subjectType: 'deal',
  subjectId: 'deal_987',
  metadata: { region: 'us-west', tier: 'enterprise' },
})

Instantiate a template with overrides

Behind CLOCKWORK_PLANNING_V2. With templateId, three optional fields customise what the template's latest version turns into, without publishing a new version:

FieldTypeMeaning
overridesRecord<string, TemplateNodeOverride | TemplateDependencyOverride>Keyed by a manifest node key: any of title (alias name), description, durationMinutes, bufferAfterMinutes, isFixed, offsetFromAnchorMin. Keyed by "from->to": { lagMinutes } for the dependency(ies) between those two nodes. Only these fields exist; any other field is a 400.
excludestring[]Node keys to skip: no task is created for them.
excludeMode'bridge' | 'drop'What happens to the dependencies around an excluded node. Default 'bridge'.
roleBindingsRecord<string, string>Role slot name -> resource id, bound while the job is created.
typescript
const { data: job } = await client.workflowInstances.create({
  templateId: template.id,
  anchorDate: '2026-03-02T08:00:00Z',
  overrides: { run: { durationMinutes: 120 }, 'prep->run': { lagMinutes: 20 } },
  exclude: ['pack'],
  roleBindings: { operator: operatorResourceId },
})
console.log(job.applied) // { overrides: ['prep->run', 'run'], excluded: ['pack'], excludeMode: 'bridge', bridgedEdges: 0, droppedEdges: 1, roleBindings: [...], tasks: { prep: '...', run: '...' } }

A runnable version is in the repository at docs-examples/f8-template-overrides.mts.

Semantics

  • Order of application. Overrides are applied first, then excludes, then the result is scheduled once. Everything that can be rejected is rejected before anything is written.
  • Excluding with bridge (default). Every predecessor of an excluded node is connected to every successor of it, and the new dependency's lag is the sum of the lags along the way, so ordering is preserved. The excluded node's own duration drops out (it no longer takes time). A run of several excluded nodes collapses into one dependency per predecessor/successor pair. The dependency type is kept; if the dependencies on one bridged path have different types the request is a 422 BRIDGE_MIXED_TYPES (use excludeMode: 'drop'). If a bridge duplicates a dependency that already exists (same pair and type), the two merge and the larger lag wins. Excluding a first or last node simply removes its dependencies.
  • Excluding with drop. Every dependency touching an excluded node is removed and nothing replaces it, so the remaining nodes may become independent of each other.
  • Dependency overrides use the lag before bridging, so an overridden lag is the one summed into a bridge (in bridge mode you may override a dependency that touches an excluded node). A from->to key that matches several dependencies (different types) overrides all of them.
  • Role bindings use the existing role-slot machinery, the same as roles.fill: each entry creates an active role assignment for the job, visible in roles.listForInstance and announced with the usual role.filled event. "Bound" means exactly that. It does not assign the nodes' tasks to the resource and does not change the schedule. applied.roleBindings[].nodeKeys lists the surviving nodes whose roleSlot is that slot (empty if they were all excluded; the binding is still made).
  • Atomic. The resulting graph is created through the same single-transaction path as createWithGraph: one schedule computation, and anchor, schedulingDirection and resourceLeveling are honoured. If anything fails, no job, task, dependency or role assignment is left behind. Only new tasks are created, so nothing already started or completed can move.
  • Deterministic. The same request produces the same schedule.
  • anchorDate plus each node's offsetFromAnchorMin is the earliest a node may start (never earlier than now), as for a plain template instantiation.

Response

When overrides, exclude or roleBindings is present (even empty), the instance comes back with two extra properties; otherwise the response is the plain instance, as before.

PropertyDescription
applied.overridesNode keys and from->to keys that carried an override
applied.excluded, applied.excludeModeWhat was excluded, and how
applied.bridgedEdgesDistinct dependencies created by bridging
applied.droppedEdgesDependencies removed because they touched an excluded node
applied.roleBindings{ slotName, resourceId, nodeKeys }[]
applied.tasksNode key -> created task id (excluded nodes are absent)
scheduleThe computed schedule, as for createWithGraph; each ref is the node key. Check schedule.conflicts: an unmeetable anchor is still a 201

Each created task stores its node key in templateNodeKey.

Errors

Status / codeWhen
404 NOT_FOUNDTemplate not found in the project; a role-binding resource that does not exist or belongs to another project (resourceIds lists them)
422 UNKNOWN_REFUnknown node or dependency key in overrides or exclude, or an undeclared role slot in roleBindings; refs lists all of them
422 ALL_EXCLUDEDEvery node is excluded
422 OVERRIDE_ON_EXCLUDEDAn override for an excluded node (or, with drop, for a dependency touching one)
422 BRIDGE_MIXED_TYPESA bridged path mixes dependency types
422 CYCLE_DETECTEDThe manifest has a dependency cycle
422 ANCHOR_REQUIREDschedulingDirection: 'backward' without anchor
422 TEMPLATE_REQUIRED, 422 TEMPLATE_EMPTYTemplate options without templateId; the template version has no nodes
400 VALIDATION_ERRORWrong types, an override field that does not exist, a role binding that is not a UUID

Known limitations

  • Manifest nodes have no resource requirements, maxLagMinutes or per-node role fields, and this feature does not add them: overrides can only change fields that exist on a node (or a dependency's lagMinutes). Use tasks.setRequirements on the created tasks afterwards.
  • Role slots are the older mechanism that capability requirements are replacing; role bindings use it because that is where templates declare their slots today. A person is bound through their resource (a resource with a platformUserId).
  • Instantiation without any of the three template options keeps the original task-by-task path (not atomic). Add roleBindings: {} to opt in to the atomic path without customising anything.
  • Overrides apply to one instantiation only; they never change the template or its versions.
  • Without the flag, none of this applies and the fields are ignored.

workflowInstances.get(instanceId)

typescript
const { data: instance } = await client.workflowInstances.get(instanceId)
console.log(instance.status)      // 'running'
console.log(instance.completedAt) // null (still running)

workflowInstances.list(opts?)

Options

OptionTypeDescription
projectIdstringFilter by project
statusWorkflowInstanceStatusFilter by status
subjectIdstringFind all instances for a specific subject
typescript
// Find all running onboarding workflows for a customer
const { data: instances } = await client.workflowInstances.list({
  subjectId: 'cust_xyz',
  status: 'running',
})

workflowInstances.cancel(instanceId)

Cancels the instance and marks all open tasks as cancelled. This cannot be undone.

typescript
const { data: instance } = await client.workflowInstances.cancel(instanceId)
console.log(instance.status) // 'cancelled'

workflowInstances.recompute(instanceId)

Replans one planner-managed job now, by the same path a task change takes (reason recompute), and returns the fresh summary. Needs planning v2.

typescript
const { data } = await client.workflowInstances.recompute(instanceId)
console.log(data.requiredStart, data.makespanMinutes, data.conflicts.length, data.changed)
  • Result. { instanceId, requiredStart, makespanMinutes, conflicts, changed }; changed is the number of tasks whose planned window moved (0 when nothing changed). A schedule.changed event with reason: "recompute" is emitted only when something moved.
  • Idempotent and safe. Calling it again changes nothing. Started, completed, cancelled and fixed tasks never move. Two simultaneous calls for one job queue behind each other: neither fails and only one emits.
  • Which jobs. Planner-managed jobs only (schedulingDirection: "backward" or resourceLeveling: "delay"). Any other job answers 422 NOT_PLANNER_MANAGED, because task changes on those recompute per connected component with the older CPM engine, not as a whole; so does a cancelled job. A completed job is allowed (its tasks are all frozen).
  • Access. Same as other job changes: a member of the job's organization. A project-bound API key sees only its own project's jobs (404 elsewhere); a guest token gets 403; an unknown job is 404; 404 while planning v2 is off.
  • When to call it. After a resource edit made with recompute: false, after editing resources directly in a way that skips the API, or to be sure a background replan finished. Resource edits otherwise trigger it for you (see Resources).

Instance status lifecycle

pending → running → completed
                  ↘ failed
                  ↘ cancelled
                  ↘ blocked   (all open tasks are blocked)

workflowInstances.createWithGraph(input)

Creates a job with its resources, tasks and dependencies atomically, computes the schedule once, and returns the instance, the ref -> id maps and the full schedule. It is the only way to create the tasks together with an anchor, a scheduling direction and resource leveling in one request (with the platform flag on, create also stores the three fields on an empty job).

POST /api/v1/workflow-instances/graph. Behind the CLOCKWORK_PLANNING_V2 flag on the platform: every planning-v2 endpoint returns 404 while the flag is off (it is off by default). This feature is merged but not yet deployed to production, so the hosted platform does not serve it yet.

A key needs the executions:write scope. For a guided walk-through see Schedule a whole workflow in one call; for adding to an existing job see Batch Graphs.

Example

A print shop. The press has 2 slots and both print runs need both of them. The operator is only needed to load and unload each run (the first and last 10 minutes), not for the whole run. Delivery is due at a fixed time, so the plan is built backward from it, and leveling keeps the press within its 2 slots. Runnable: docs-examples/wi-create-with-graph.mts.

typescript
const { data } = await client.workflowInstances.createWithGraph({
  instance: {
    name: 'Order 4471',
    subjectType: 'order',
    subjectId: '4471',
    jobValue: 1200, // whole currency units
    metadata: { customer: 'Acme Books' },
    anchor: { type: 'finish', at: due.toISOString() },
    schedulingDirection: 'backward',
    resourceLeveling: 'delay',
    // projectId: '…'  defaults to the caller's own project
  },
  resources: [
    {
      ref: 'press', name: 'Press', type: 'asset', capacity: 2,
      description: 'Two-slot offset press',
      availability: [{ from: windowFrom, to: due.toISOString() }],
      properties: { model: 'OP-2' },
      metadata: { site: 'north' },
      externalId: 'press-1',
    },
    { ref: 'operator', name: 'Operator', type: 'human', capacity: 1 },
  ],
  tasks: [
    {
      ref: 'plates', name: 'Make plates', durationMinutes: 30,
      requirements: [{ resourceRef: 'operator', units: 1 }],
    },
    {
      ref: 'run', name: 'Print run', durationMinutes: 120,
      description: 'Both slots, operator at start and end only',
      bufferAfterMinutes: 15, // ink-drying time that counts toward the anchor
      tags: ['press'],
      priority: 'high',
      subjectType: 'order', subjectId: '4471',
      metadata: { sheets: 5000 },
      resourceRefs: ['press'], // links the resource; it does not claim capacity (see `requirements`)
      requirements: [
        { resourceRef: 'press', units: 2 },
        { resourceRef: 'operator', units: 1, occupancy: { mode: 'bookends', startMinutes: 10, endMinutes: 10 } },
      ],
    },
    {
      ref: 'bind', name: 'Bind and box', durationMinutes: 60,
      earliestStart: new Date(Date.now() + 3_600_000).toISOString(), // lower bound for the start
      deadline: due.toISOString(),
      anchorWindow: { minOffsetMinutes: -10, maxOffsetMinutes: 0, applyTo: 'finish' }, // finish 0 to 10 min before the anchor
      requirements: [{ resourceRef: 'operator', units: 1 }],
    },
  ],
  dependencies: [
    { from: 'plates', to: 'run' },
    { from: 'run', to: 'bind', type: 'finish_to_start', lagMinutes: 0, maxLagMinutes: 240 },
  ],
})

Request

The body is an object with four keys. Only tasks is required (at least one task). Every object in the request is strict: an unknown field anywhere is a 400 VALIDATION_ERROR, never silently ignored.

KeyTypeRequiredDefaultDescription
instanceobjectno{}The job itself. See instance.
resourcesGraphResourceInput[]no[]Resources to create with the job. At most 100.
tasksGraphTaskInput[]yesAt least 1, at most 500.
dependenciesGraphDependencyInput[]no[]At most 2,000.

ref values are keys you choose (1 to 100 characters). Task refs must be unique among tasks, resource refs unique among resources; a task ref and a resource ref may be equal. Dependencies and requirements point at refs, and the response maps every ref to the id the platform generated.

Timestamps (anchor.at, earliestStart, deadline, availability bounds) are ISO 8601 UTC strings ending in Z, for example 2026-11-26T16:00:00Z. An offset such as +02:00 is rejected with 400.

instance

FieldTypeRequiredDefaultDescription
anchor{ type: 'finish'; at: string }nononeThe required finish of the whole plan ("everything done by 16:00"). type must be 'finish'.
schedulingDirection'forward' | 'backward'no'forward''backward' places every task at its latest start that still meets the anchor. Requires anchor (otherwise 422 ANCHOR_REQUIRED).
resourceLeveling'off' | 'delay'no'off''off' only reports over-allocation of a resource (a resource_capacity_breach conflict). 'delay' resolves it by delaying tasks that have not started.
assignmentPolicy'balance_load' | 'earliest_finish'nononeResolve each requirement on a capability:<id> pool to one member resource, returned in schedule.assignments. Requires resourceLeveling: 'delay' (otherwise 422 ASSIGNMENT_REQUIRES_LEVELING). Omitted: a pool requirement stays a claim on the pool's total capacity. See Assign from a pool.
namestringnonullDisplay name.
projectIdstring (UUID)nocaller's project404 NOT_FOUND ("Project not found") if it does not exist.
subjectType, subjectIdstringnonullOpaque labels for your own entity.
jobValueintegernonullWhole-number value of the job.
metadataRecord<string, unknown>no{}Arbitrary key-value data.

An anchor with schedulingDirection: 'forward' is stored but does not move tasks on its own; it matters to anchorWindow and to backward planning. The job is created with status: 'running' and startedAt set to the time of the request.

resources[]

FieldTypeRequiredDefaultDescription
refstringyes1 to 100 characters, unique among resources.
namestringyes
type'human' | 'agent' | 'service' | 'asset'one of type / resourceTypeIdA request with neither is 400 ("Either type or resourceTypeId is required").
resourceTypeIdstring (UUID)one of type / resourceTypeIdAn existing resource type.
capacitypositive integernounlimitedUnits available at once (the press has 2). A resource without a capacity is never reported as over-allocated.
descriptionstringnonull
availabilityunknown[]noalways availableAbsolute windows { from, to } or { start, end } (ISO 8601). With at least one recognized window the resource can only be used inside their union. An entry that looks like a window but has bad timestamps, or ends before it starts, is a 400. Other entry shapes are stored and ignored by the planner.
properties, metadataRecord<string, unknown>noStored as given.
externalIdstringnonullYour own id for the resource.
platformUserIdstring (UUID)nonullA platform user id stored on the resource.
stateKey, stateTolerancestring, number >= 0noShared state the resource tracks (a temperature, a setting). Must be given together. Claims may overlap only when their state values are within the tolerance.
stateSetupMinutesinteger >= 0noChangeover minutes between claims whose states differ by more than the tolerance. Needs stateKey.

tasks[]

FieldTypeRequiredDefaultDescription
refstringyes1 to 100 characters, unique among tasks.
namestringyesNon-empty.
descriptionstringno
durationMinutesinteger >= 0no0
durationSecondsinteger >= 0noWhen set, used instead of durationMinutes.
isFixedbooleannofalseA fixed task never moves: it is planned at its earliestStart.
earliestStartISO UTC stringnotime of the requestLower bound for the start.
deadlineISO UTC stringnononeA task that cannot finish by it raises a deadline_violated conflict.
assigneePlatformUserIdstring (UUID)nonone
bufferAfterMinutesinteger >= 0no0Gap kept after the task. Counts toward the anchor.
bufferAfterSecondsinteger >= 0noWhen set, used instead of bufferAfterMinutes.
tagsstring[]no[]
priority'high' | 'medium' | 'low' | nullnonull
subjectType, subjectIdstring | nullnonull
metadataRecord<string, unknown>no
resourceRefsstring[]no[]Refs of resources in this request assigned to the task. This links the resource to the task; it does not claim capacity. Use requirements for that. An unknown ref is 422 UNKNOWN_REF.
requirementsGraphRequirementInput[]no[]Units-and-occupancy claims on resources. At most 50 per task.
anchorWindowobjectnononeShorthand for an anchor_window constraint on this task. Requires instance.anchor (422 ANCHOR_REQUIRED).

anchorWindow:

FieldTypeRequiredDefaultDescription
minOffsetMinutes, maxOffsetMinutesintegerat least oneOffsets from the anchor in minutes; negative is before it. minOffsetMinutes must not exceed maxOffsetMinutes.
applyTo'finish' | 'start'no'finish'Whether the window bounds the task's finish or its start.

requirements[]: set exactly one of resourceRef, resourceId or poolId.

FieldTypeRequiredDefaultDescription
resourceRefstringone targetA resource created in this request.
resourceIdstring (UUID)one targetAn existing resource. 422 UNKNOWN_REF if it does not exist.
poolIdstringone targetA pool id such as capability:{capabilityId}. By default a requirement claims capacity on the pool as a whole; with instance.assignmentPolicy the planner picks one member of the pool for it.
unitspositive integerno1Must not exceed the resource's capacity (422 REQUIREMENT_EXCEEDS_CAPACITY).
occupancyobjectno{ mode: 'full' }When within the task the units are held: { mode: 'full' } (the whole duration), { mode: 'bookends', startMinutes, endMinutes } (only the first and last minutes; both required, integers >= 0) or { mode: 'none' } (never; declared for visibility).
statenumbernoThe shared-state value this claim needs. The resource must declare stateKey (422 STATE_NOT_SUPPORTED otherwise); not valid on a pool.

dependencies[]

FieldTypeRequiredDefaultDescription
from, tostringyesTask refs. from is the predecessor.
type'finish_to_start' | 'start_to_start' | 'finish_to_finish' | 'start_to_finish'no'finish_to_start'
lagMinutesintegerno0Minimum gap. May be negative (overlap).
lagSecondsinteger >= 0noWhen set, used instead of lagMinutes.
maxLagMinutesintegernononeMaximum gap, >= lagMinutes (else 400). The planner moves the predecessor later to keep the gap within it; when it cannot (for example the predecessor is isFixed) the plan keeps the long gap and reports a max_lag_exceeded conflict.

Response

201 Created with { data }. data has exactly four keys:

KeyTypeDescription
instanceWorkflowInstanceThe created job, including anchor, schedulingDirection, resourceLeveling, requiredStart and makespanMinutes.
refsRecord<string, string>Task ref to task id.
resourceRefsRecord<string, string>Resource ref to resource id. Empty when the request created no resources.
scheduleGraphScheduleThe computed schedule.

schedule has exactly these keys:

KeyTypeDescription
tasksGraphScheduleTask[]One row per task, sorted by ref.
requiredStartstring | nullSee below.
finishstring | nullThe latest planned task end (buffers after a task are not included).
makespanMinutesnumberSee below.
criticalPathstring[]Task ids of the chain that determines the finish, in planned order. It is a single path, and exists even when an anchor leaves every task with slack.
utilizationResourceUtilization[]One row per capacity-limited resource, sorted by resource id.
conflictsGraphConflict[]Everything the plan could not satisfy. Empty when the plan is clean.
assignmentsPoolAssignment[]Only with an assignmentPolicy. { ref, taskId, requirementId, poolId, assignedResourceId } for every pool requirement that got a member, sorted by task ref. A requirement no member can hold is absent.

schedule.tasks[] has the keys ref, name, taskId, plannedStart, plannedEnd, earliestStart, latestStart, slackSec and isCritical:

FieldDescription
plannedStart, plannedEndWhere the plan placed the task (whole seconds).
earliestStartThe earliest the task could start: your lower bound pushed by its predecessors.
latestStartThe latest it could start and still meet the anchor or deadline.
slackSeclatestStart - earliestStart in seconds. Negative when the plan cannot be met.
isCriticaltrue when slackSec <= 0. This is per task, so it can be false for every task of a plan whose anchor leaves slack, while criticalPath still lists the driving chain.

schedule.tasks[].name currently echoes the task's ref, not its name. Read the name from the task itself, or from the request you sent. (scheduling.simulate returns the real name.)

schedule.utilization[] has resourceId, resourceRef, capacity, occupiedUnitMinutes (units times occupied time, so a bookends claim counts only its first and last minutes), availableUnitMinutes (capacity times the plan span), utilization (occupied divided by available, above 1 only when over-allocated) and peakUnits (the most units in use at any instant).

schedule.conflicts[] has taskId, ref, type, description, slackViolationSec, and for anchor_infeasible also shortfallMinutes. Types you can see here: deadline_violated, resource_capacity_breach, constraint_violated, anchor_infeasible and max_lag_exceeded. A plan that cannot be met is still a 201: check schedule.conflicts on every response. The same conflicts are persisted and can be listed with conflicts.list().

requiredStart and makespanMinutes

  • requiredStart: the earliest plannedStart among the job's tasks that are not cancelled, as a UTC timestamp with milliseconds. In other words, when the first piece of work must begin. In a backward plan it is the latest moment the job can start and still meet the anchor; in a forward plan it is when the first task starts (the request time, or the largest earliestStart bound of the first task). It is null only when there are no live tasks, which a successful createWithGraph never produces. When a backward plan cannot meet its anchor, requiredStart falls back to the earliest start the tasks' lower bounds allow (the request time unless you set earliestStart) and an anchor_infeasible conflict reports the shortfall.
  • makespanMinutes: whole minutes from requiredStart to finish (the latest planned task end), rounded to the nearest minute (225.0 stays 225, 3.75 becomes 4). Gaps between tasks (lags, buffers between tasks, waiting for resources) are inside it; a buffer after the very last task is not. schedule.makespanMinutes is 0 for an empty plan. The instance's makespanMinutes is null when the instance has never been planned as a whole, for example one made with create.

The same two values are stored on the instance (instance.requiredStart, instance.makespanMinutes). They are refreshed whenever an anchored or leveled job is re-planned (after a delay, a completion, an added task). For a plain forward job they keep the values from creation, and its tasks' plannedStart / plannedEnd are likewise a snapshot from creation: read earliestStart for the current forward schedule.

Atomicity and rollback

The request is validated entirely in memory first: field validation, then reference, duplicate, anchor, capacity and cycle checks, then the schedule is computed. Only if all of that passes are rows written, in one database transaction (job, resources, tasks, constraints, requirements, dependencies, change log and any conflicts). If any step fails nothing is created: a 4xx leaves the database untouched, and a failure inside the transaction rolls everything back and returns 500 INTERNAL_ERROR. Events (workflow.started, and conflict_detected when the plan has conflicts) and usage meters are emitted after the commit, so a failed request emits nothing.

How many schedule computations?

Exactly one, however many tasks and dependencies the request holds (a single planSchedule pass; the service counts it once). Building the same job task by task would recompute the schedule after every task and every dependency. The platform's own test asserts a 150-task, 250-dependency request produces exactly one computation. This is an internal count, not something an HTTP client can observe. A job created this way does not emit schedule.changed; that event describes later changes.

Errors

StatuscodeWhenExtra fields
400VALIDATION_ERRORMalformed or missing body, an unknown field, a value out of range or of the wrong type, more than the maximum tasks, dependencies or resources, a +02:00-style timestampdetails.issues[] (each with path and message)
401 / 403UNAUTHORIZED / FORBIDDENNo credentials, or the key lacks executions:write
404NOT_FOUNDThe planning flag is off ("Not found"), or instance.projectId does not exist ("Project not found")
422UNKNOWN_REFA dependency names a task ref that is not in tasks; a task names a resource ref that is not in resources; a resourceId does not existrefs
422DUPLICATE_REFTwo tasks, or two resources, share a refrefs
422CYCLE_DETECTEDThe dependencies form a cycle (a task depending on itself is one)refs (the tasks on the cycle)
422ANCHOR_REQUIREDschedulingDirection: 'backward', or a task anchorWindow, without instance.anchorrefs (the tasks, or empty)
422REQUIREMENT_EXCEEDS_CAPACITYA requirement asks for more units than the resource's capacityrefs (task, resource)
422STATE_NOT_SUPPORTEDA requirement gives a state on a pool, or on a resource with no stateKeyrefs
422ASSIGNMENT_REQUIRES_LEVELINGinstance.assignmentPolicy without resourceLeveling: 'delay'refs (empty)
429RATE_LIMITEDThe key's per-minute budget is used up
500INTERNAL_ERRORA failure inside the transaction (everything rolled back)

Exact messages seen, for matching in logs:

text
422 UNKNOWN_REF                    Dependencies reference unknown task refs: ghost
422 UNKNOWN_REF                    Tasks reference unknown resource refs: nope, nope2
422 DUPLICATE_REF                  Duplicate task refs: a
422 DUPLICATE_REF                  Duplicate resource refs: r
422 CYCLE_DETECTED                 Dependency cycle: a -> b -> c -> a
422 ANCHOR_REQUIRED                schedulingDirection 'backward' requires instance.anchor
422 ANCHOR_REQUIRED                anchorWindow requires instance.anchor (tasks: a)
422 REQUIREMENT_EXCEEDS_CAPACITY   Task "a" requires 3 units of "p", which has capacity 2
400 VALIDATION_ERROR               (no message field; see details.issues, e.g. "Unrecognized key(s) in object: 'bogus'")

Every problem found by the reference, duplicate, anchor, capacity and state checks is listed in errors (each with code, message, refs); the top-level code, message and refs describe the first. Cycle detection only runs when those checks found nothing. In the SDK these arrive as a ClockworkError with status, code and message; the extra fields are in details (so details.refs). A 400 has no message field, so the SDK's message is HTTP 400: read details.issues. A body that is not valid JSON is read as an empty object and fails as 400 with a "Required" issue on tasks.

Limits, request size and rate limits

LimitValueOver the limit
Tasks per request500 (at least 1)400 VALIDATION_ERROR, "Array must contain at most 500 element(s)"
Dependencies per request2,000400 VALIDATION_ERROR, "Array must contain at most 2000 element(s)"
Resources per request100400 VALIDATION_ERROR, "Array must contain at most 100 element(s)"
Requirements per task50400 VALIDATION_ERROR
ref length100 characters400 VALIDATION_ERROR

A request at the maximum (500 tasks, 2,000 dependencies, 100 resources) succeeded in under a second on a local server; its response was about 200 KB. The platform code sets no request-body size limit of its own; the limit that applies to a deployed request is the hosting platform's (not verified here).

The call is one request toward your rate limit: the limiter is checked once per HTTP request, before the handler runs, and a 500-task graph costs the same as a one-task graph. Success responses and 400s carry the RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers; a 422 currently does not. Without a configured rate-limit store the platform fails open and never returns 429.

Type reference

typescript
type WorkflowInstanceStatus = 'pending' | 'running' | 'blocked' | 'completed' | 'failed' | 'cancelled'
type SchedulingDirection = 'forward' | 'backward'
type ResourceLeveling = 'off' | 'delay'
 
interface WorkflowInstance {
  id: string
  templateId: string | null
  projectId: string
  name: string | null
  status: WorkflowInstanceStatus
  startedAt: string | null
  completedAt: string | null
  subjectType: string | null
  subjectId: string | null
  jobValue: number | null
  requiredStart?: string | null     // earliest planned task start from the last whole-job plan; null if never planned
  makespanMinutes?: number | null   // minutes from requiredStart to the last planned end; null if never planned
  anchor?: { type: 'finish'; at: string } | null  // required finish of the whole plan; set only by createWithGraph
  schedulingDirection?: SchedulingDirection        // 'forward' unless set by createWithGraph
  resourceLeveling?: ResourceLeveling              // 'off' unless set by createWithGraph
  assignmentPolicy?: AssignmentPolicy              // present only when set by createWithGraph
  metadata?: Record<string, unknown> | null
  createdAt: string
  updatedAt: string
}
 
interface CreateWorkflowGraphInput {
  instance?: {
    anchor?: { type: 'finish'; at: string }
    schedulingDirection?: SchedulingDirection
    resourceLeveling?: ResourceLeveling
    assignmentPolicy?: AssignmentPolicy   // 'balance_load' | 'earliest_finish'; needs resourceLeveling 'delay'
    projectId?: string
    name?: string
    subjectType?: string
    subjectId?: string
    jobValue?: number
    metadata?: Record<string, unknown>
  }
  resources?: GraphResourceInput[]
  tasks: GraphTaskInput[]
  dependencies?: GraphDependencyInput[]
}
 
interface CreateWorkflowGraphResult {
  instance: WorkflowInstance
  refs: Record<string, string>          // task ref -> task id
  resourceRefs: Record<string, string>  // resource ref -> resource id
  schedule: GraphSchedule
}
 
interface GraphSchedule {
  tasks: GraphScheduleTask[]
  requiredStart: string | null
  finish: string | null
  makespanMinutes: number
  criticalPath: string[]
  utilization: ResourceUtilization[]
  conflicts: Array<{
    taskId: string; ref: string; type: string; description: string
    slackViolationSec: number
    shortfallMinutes?: number           // anchor_infeasible only
  }>
}
 
interface GraphScheduleTask {
  ref: string; name: string; taskId: string
  plannedStart: string; plannedEnd: string
  earliestStart: string; latestStart: string
  slackSec: number; isCritical: boolean
}
 
interface ResourceUtilization {
  resourceId: string; resourceRef?: string; capacity: number
  occupiedUnitMinutes: number; availableUnitMinutes: number
  utilization: number; peakUnits: number
}

The request types (GraphResourceInput, GraphTaskInput, GraphRequirementInput, GraphDependencyInput, AnchorWindowInput) are exported from @clockwork/sdk and match the tables above.