Dependencies
A Dependency defines an ordering constraint between two tasks. When you add a dependency from task A to task B, Clockwork ensures B cannot start until A reaches the required state — and re-schedules B automatically whenever A's timing changes.
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.dependencies
- Modeling sequential workflows where step N must finish before step N+1 starts
- Expressing parallel branches that converge at a common later task
- Adding lag time between tasks (e.g., "wait 24 hours after sending the email before following up")
- Using start-to-start dependencies for tasks that should begin together
Methods
| Method | Description |
|---|---|
create(input) | Define ordering between two tasks |
createBatch(input) | Link up to 2,000 task pairs in one atomic request (one request toward rate limits) |
get(depId) | Fetch a dependency |
update(depId, input) | Update lag, maximum lag or dependency type |
delete(depId) | Remove the constraint |
list(opts?) | List dependencies, optionally those touching one task |
dependencies.create(input)
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
fromTaskId | string | yes | The predecessor task |
toTaskId | string | yes | The successor task |
type | DependencyType | no | Dependency type (default: 'finish_to_start') |
lagMinutes | integer | no | Minimum wait between predecessor and successor, default 0 (the server accepts a negative value, an overlap; update does not) |
lagSeconds | integer >= 0 | no | Finer-grained lag. In the REST body and createBatch entries; not in the SDK's CreateDependencyInput type |
maxLagMinutes | integer | no | Maximum wait (must be >= lagMinutes). See Maximum lag |
Validation: a body that fails the schema (a non-UUID id, maxLagMinutes below lagMinutes) is 400 VALIDATION_ERROR; an unknown task is 404 NOT_FOUND; linking a task you cannot see is 403 FORBIDDEN; a link that would close a cycle is 400 CYCLE_DETECTED.
Dependency types
| Type | Meaning |
|---|---|
finish_to_start | B starts after A finishes (most common) |
start_to_start | B starts no earlier than A starts |
finish_to_finish | B finishes no earlier than A finishes |
start_to_finish | B finishes no earlier than A starts |
Maximum lag
maxLagMinutes bounds the gap the dependency type relates — for finish_to_start, how long after A finishes B may start. "Bake no more than 24 hours after assembling" is maxLagMinutes: 1440.
await client.dependencies.create({ fromTaskId: assemble, toTaskId: bake, maxLagMinutes: 1440 })
await client.dependencies.update(depId, { maxLagMinutes: null }) // clear itmaxLagMinutes is accepted on create, createBatch, update and on dependencies entries of createWithGraph. update takes null to clear it (sending the field absent leaves it unchanged), and re-checks the rule against the resulting values: lowering lagMinutes never fails, but raising it above an existing maxLagMinutes, or setting maxLagMinutes below lagMinutes, is 422 VALIDATION_ERROR. Creating one with maxLagMinutes < lagMinutes is 400 VALIDATION_ERROR (schema check).
How it is honored depends on how the job is scheduled:
| Scheduling | Behavior |
|---|---|
Batch-graph, anchored (backward) and other planner-managed jobs | The planner keeps the gap within the limit. If the successor is held late, the predecessor is delayed (unstarted work only); in backward mode a predecessor pulled early by another successor pulls this successor early too. |
| Incremental edits on other jobs | Detection only: a max_lag_exceeded conflict is raised on the successor, with the overrun in slackViolationSec. |
| Predecessor already started or done | It cannot move, so the limit cannot be honored: you get a max_lag_exceeded conflict. |
Started and completed tasks never move.
Example
// Basic sequential dependency
await client.dependencies.create({
fromTaskId: 'tsk_setup',
toTaskId: 'tsk_training',
})
// With a 24-hour lag (send email, then follow up the next day)
await client.dependencies.create({
fromTaskId: 'tsk_send_email',
toTaskId: 'tsk_follow_up',
type: 'finish_to_start',
lagMinutes: 1440, // 24 hours
})
// Parallel tasks that must start together
await client.dependencies.create({
fromTaskId: 'tsk_kickoff',
toTaskId: 'tsk_design_track',
type: 'start_to_start',
})
await client.dependencies.create({
fromTaskId: 'tsk_kickoff',
toTaskId: 'tsk_engineering_track',
type: 'start_to_start',
})dependencies.createBatch(input)
Links many task pairs in one request (1 to 2,000 entries). The whole batch is checked for cycles together with the existing graph (422 CYCLE_DETECTED, with the task ids on the cycle), inserted in one statement, and each affected connected component is recomputed once. Counts as one request toward rate limits.
Each entry takes fromTaskId, toTaskId and optionally type, lagMinutes, lagSeconds and maxLagMinutes. The body is strict: an unknown field is 400 VALIDATION_ERROR. An unknown task id is 404 NOT_FOUND (with the missing ids); a task you cannot see is 403 FORBIDDEN. The endpoint answers 404 while planning v2 is off.
await client.dependencies.createBatch({
dependencies: [
{ fromTaskId: a, toTaskId: b },
{ fromTaskId: b, toTaskId: c, lagMinutes: 10 },
],
})dependencies.update(depId, input)
await client.dependencies.update('dep_abc123', {
lagMinutes: 2880, // increase to 48 hours
})update accepts lagMinutes (integer >= 0), type and maxLagMinutes (integer or null). fromTaskId and toTaskId cannot be changed: delete and re-create the dependency. It recomputes the schedule. A dependency you cannot see is 404 NOT_FOUND.
dependencies.list(opts?)
// Find all dependencies involving a task (as predecessor or successor)
const { data: deps } = await client.dependencies.list({ taskId: 'tsk_abc123' })Cycle detection
Adding a dependency that would create a cycle throws a ClockworkError with code CYCLE_DETECTED: status 400 from create, status 422 from createBatch and createWithGraph. Always validate your graph structure when building workflows programmatically.
try {
await client.dependencies.create({ fromTaskId: 'tsk_b', toTaskId: 'tsk_a' })
} catch (err) {
if (err instanceof ClockworkError && err.code === 'CYCLE_DETECTED') {
console.error('This dependency would create a cycle')
}
}Type reference
type DependencyType = 'finish_to_start' | 'start_to_start' | 'finish_to_finish' | 'start_to_finish'
interface Dependency {
id: string
fromTaskId: string
toTaskId: string
type: DependencyType
lagMinutes?: number | null
maxLagMinutes?: number | null // null / absent = unbounded
createdAt: string
}
interface CreateDependencyInput {
fromTaskId: string
toTaskId: string
type?: DependencyType
lagMinutes?: number
maxLagMinutes?: number
}
interface UpdateDependencyInput {
lagMinutes?: number
type?: DependencyType
maxLagMinutes?: number | null // null clears it
}Related
- Tasks — the tasks being linked
- Conflicts — dependency violations surface as conflicts (
max_lag_exceeded) - Batch Graphs — create tasks and dependencies together