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.

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
  const { data: approval } = await client.approvals.get(id)
  if (approval.taskId) {
    await client.tasks.create({
      name: 'Revise contract per legal feedback',
      workflowInstanceId: /* get from task */,
      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 })
}