Human-in-the-Loop

Clockwork is built for workflows that mix automation with human judgment. This guide shows how to pause a workflow for human review, require approval before proceeding, and notify the right person at the right time.

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.

The pattern

Automated task → Approval gate → Human reviews → Approve/Reject → Next task

Approval gates block the workflow without polling. The workflow resumes only when someone calls approvals.approve() or approvals.reject().

Example: contract review workflow

typescript
import { Clockwork } from '@clockwork/sdk'
 
const client = new Clockwork({ apiKey: process.env.CLOCKWORK_API_KEY })
 
async function startContractReview(dealId: string, contractUrl: string) {
  // 1. Create the workflow instance
  const { data: instance } = await client.workflowInstances.create({
    subjectType: 'deal',
    subjectId: dealId,
  })
 
  // 2. First task: AI drafts the summary
  const { data: summaryTask } = await client.tasks.create({
    name: 'Generate contract summary',
    workflowInstanceId: instance.id,
    durationMinutes: 5,
  })
 
  // 3. Attach the contract as a required artifact
  const { data: artifact } = await client.artifacts.create({
    name: 'Contract Draft',
    type: 'document',
    workflowInstanceId: instance.id,
    taskId: summaryTask.id,
    contentReference: contractUrl,
    required: true,
  })
 
  // 4. Create the review task — blocked until the approval resolves
  const { data: reviewTask } = await client.tasks.create({
    name: 'Legal review',
    workflowInstanceId: instance.id,
    durationMinutes: 120,
  })
 
  // 5. Summary must complete before legal review starts
  await client.dependencies.create({
    fromTaskId: summaryTask.id,
    toTaskId: reviewTask.id,
  })
 
  // 6. Complete the summary task (AI has finished)
  await client.tasks.complete(summaryTask.id)
 
  // 7. Create an approval gate tied to the artifact
  const { data: approval } = await client.approvals.create({
    taskId: reviewTask.id,
    artifactId: artifact.id,
    reason: 'Contract requires legal sign-off before countersigning',
  })
 
  // 8. Notify the legal team
  await notifyLegalTeam({
    approvalId: approval.id,
    reviewTaskId: reviewTask.id,
    contractUrl,
    dealId,
  })
 
  return { instanceId: instance.id, approvalId: approval.id }
}

Building the approval UI

When the legal team receives the notification, they need to approve or reject:

typescript
// In your API handler when the reviewer clicks "Approve"
app.post('/approvals/:id/approve', async (req, res) => {
  const { id } = req.params
  const { userId, comment } = req.body
 
  await client.approvals.approve(id, {
    decidedByActorId: userId,
    reason: comment,
  })
 
  res.json({ success: true })
})
 
// When they click "Reject"
app.post('/approvals/:id/reject', async (req, res) => {
  const { id } = req.params
  const { userId, comment } = req.body
 
  await client.approvals.reject(id, {
    decidedByActorId: userId,
    reason: comment,
  })
 
  // Optionally: create a revision task
  // (There is no approvals.get: list and pick the one you decided.)
  const { data: approvals } = await client.approvals.list()
  const approval = approvals.find((a) => a.id === id)
  if (approval?.taskId) {
    const { data: rejectedTask } = await client.tasks.get(approval.taskId)
    await client.tasks.create({
      name: 'Revise contract per legal feedback',
      workflowInstanceId: rejectedTask.workflowInstanceId ?? undefined,
      durationMinutes: 60,
      priority: 'high',
    })
  }
 
  res.json({ success: true })
})

Pending approvals inbox

typescript
async function getPendingApprovals(projectId: string) {
  const { data: approvals } = await client.approvals.list({ projectId })
 
  return approvals
    .filter(a => a.status === 'pending')
    .map(a => ({
      id: a.id,
      taskId: a.taskId,
      reason: a.reason,
      artifactId: a.artifactId,
      createdAt: a.createdAt,
    }))
}

AI + human handoff

A common pattern: an AI agent does the first pass, then a human reviews before the workflow continues.

typescript
// AI agent completes its analysis task and requests human review
async function onAiTaskComplete(taskId: string, result: string) {
  // Record the AI's output as a conversation message
  const { data: conv } = await client.conversations.create({
    parentType: 'task',
    parentId: taskId,
  })
 
  await client.conversations.postMessage(conv.id, {
    role: 'assistant',
    body: `Analysis complete:\n\n${result}\n\nPlease review and approve to continue.`,
  })
 
  // Create approval gate
  const { data: approval } = await client.approvals.create({
    taskId,
    reason: 'AI analysis requires human review before proceeding',
  })
 
  // Mark the AI task done
  await client.tasks.complete(taskId)
 
  // Notify the human reviewer
  await notifyReviewer({ approvalId: approval.id, conversationId: conv.id })
}