Creating Workflows

This guide walks through building a complete workflow from scratch — a template with role slots, an instance with tasks and dependencies, and monitoring it to completion.

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.

The pattern

Template (blueprint) → Instance (live run) → Tasks (work items)

A template is optional. You can also create instances directly and add tasks programmatically without any template.

Option A: Template-first

Use a template when you'll run the same workflow shape repeatedly.

1. Define the template

typescript
import { Clockwork } from '@clockwork/sdk'
 
const client = new Clockwork({ apiKey: process.env.CLOCKWORK_API_KEY })
 
const { data: template } = await client.workflowTemplates.create({
  name: 'New Client Onboarding',
  description: 'Standard workflow for onboarding a new client',
  roles: [
    { slotName: 'account-manager', required: true },
    { slotName: 'legal', required: true },
  ],
})

2. Publish a version with a manifest

typescript
await client.workflowTemplates.publish(template.id, {
  manifest: {
    nodes: [
      { id: 'n1', name: 'Send welcome email', durationMinutes: 15 },
      { id: 'n2', name: 'Schedule kickoff call', durationMinutes: 30 },
      { id: 'n3', name: 'Collect signed NDA', durationMinutes: 60 },
      { id: 'n4', name: 'Set up client portal', durationMinutes: 45 },
    ],
    edges: [
      { from: 'n1', to: 'n2' },
      { from: 'n2', to: 'n3' },
      { from: 'n2', to: 'n4' },
    ],
  },
  roles: [
    { slotName: 'account-manager', required: true },
    { slotName: 'legal', required: true },
  ],
})

3. Instantiate for each new client

typescript
const { data: instance } = await client.workflowInstances.create({
  templateId: template.id,
  subjectType: 'client',
  subjectId: 'client_acme',
})
 
// Fill role slots with real people
await client.roles.fill(instance.id, 'account-manager', 'res_alice')
await client.roles.fill(instance.id, 'legal', 'res_bob')

4. Customise one run (overrides, excludes, role bindings)

Behind CLOCKWORK_PLANNING_V2, a single instantiation can differ from the template without publishing a new version. Say a print-shop template runs prep -> run -> pack, and this order ships unpacked, needs a longer press run, and already has its operator:

typescript
const { data: job } = await client.workflowInstances.create({
  templateId: template.id,
  anchorDate: '2026-03-02T08:00:00Z',
  overrides: { run: { durationMinutes: 120 } }, // keyed by the manifest node key
  exclude: ['pack'],                            // no task for this node
  roleBindings: { operator: operatorResourceId }, // same effect as roles.fill, done atomically
})
 
console.log(job.applied)   // what was overridden / excluded / bound, and node key -> task id
console.log(job.schedule)  // the schedule from the single atomic computation (refs are node keys)

Excluding a node in the middle keeps the order: its predecessors are connected to its successors and the lags are summed (excludeMode: 'bridge', the default); use excludeMode: 'drop' to remove the node's dependencies instead. Unknown node keys, unknown role slots and excluding everything are rejected with a 422 before anything is written, and a failure part-way leaves nothing behind. Only fields a manifest node already has can be overridden; to give a created task resource requirements, use tasks.setRequirements afterwards. Full reference: Instantiate a template with overrides.

Option B: Programmatic (no template)

Use this when each workflow is unique or generated dynamically.

typescript
// 1. Create a bare instance
const { data: instance } = await client.workflowInstances.create({
  subjectType: 'deal',
  subjectId: 'deal_987',
})
 
// 2. Add tasks
const { data: t1 } = await client.tasks.create({
  name: 'Due diligence review',
  workflowInstanceId: instance.id,
  durationMinutes: 240,
  deadline: '2026-07-15T17:00:00Z',
  priority: 'high',
})
 
const { data: t2 } = await client.tasks.create({
  name: 'Legal sign-off',
  workflowInstanceId: instance.id,
  durationMinutes: 60,
})
 
const { data: t3 } = await client.tasks.create({
  name: 'Send countersigned agreement',
  workflowInstanceId: instance.id,
  durationMinutes: 15,
})
 
// 3. Define ordering
await client.dependencies.create({ fromTaskId: t1.id, toTaskId: t2.id })
await client.dependencies.create({ fromTaskId: t2.id, toTaskId: t3.id })
 
// 4. Assign tasks
await client.tasks.assign(t1.id, 'user_analyst')
await client.tasks.assign(t2.id, 'user_legal')

Monitoring progress

typescript
// Check instance status
const { data: instance } = await client.workflowInstances.get('wi_abc123')
console.log(instance.status) // 'running'
 
// List tasks with their scheduling info
const { data: tasks } = await client.tasks.list({ workflowInstanceId: 'wi_abc123' })
 
for (const task of tasks) {
  console.log(
    task.name,
    task.status,
    task.isCritical ? '⚠ CRITICAL' : '',
    task.slackSec !== null ? `${Math.round(task.slackSec / 3600)}h float` : '',
  )
}

Handling completion

typescript
// When a user completes a task
const { data: result } = await client.tasks.complete('tsk_abc123')
 
// Log which tasks just became available
for (const unlocked of result.unlockedTasks) {
  console.log(`Now ready: ${unlocked.name}`)
  await notifyAssignee(unlocked.id)
}

Next steps