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 })
}Related
- Approvals SDK reference
- Artifacts — attach documents to approval gates
- Conversations — add threaded discussion to review steps
- MCP Integration — connect AI agents to the full workflow toolset