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
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. 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
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
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
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:
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.
// 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
// 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
// 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
- Event-Driven Patterns — react to task events instead of polling
- Human-in-the-Loop — add approval gates to your workflow
- Conflict Resolution — respond automatically when the engine detects scheduling violations