Maintain human oversight of AI agent actions with configurable approval gates, risk-based routing, and comprehensive audit trails.
Agent Proposes Action
Workflow Pauses
Human Reviews
Workflow Resumes
// Basic HITL approval gate
import { createWorkflow, step, hitl } from "@/lib/workflows"
export const contentPublishingWorkflow = createWorkflow({
id: "content-publishing",
steps: [
// Step 1: AI generates content
step({
id: "generate-content",
agent: "content-writer",
action: async (ctx) => {
const { topic, tone, length } = ctx.trigger.data
const content = await ai.generateContent({ topic, tone, length })
return { content, generatedAt: new Date() }
}
}),
// Step 2: HITL approval gate
hitl({
id: "content-approval",
name: "Content Review",
dependsOn: ["generate-content"],
// What to show the reviewer
display: (ctx) => ({
title: "Review Generated Content",
description: "Please review the AI-generated content before publishing",
content: ctx.steps["generate-content"].output.content,
metadata: {
topic: ctx.trigger.data.topic,
tone: ctx.trigger.data.tone,
generatedAt: ctx.steps["generate-content"].output.generatedAt
}
}),
// Who can approve
approvers: {
roles: ["content-manager", "editor"],
// Or specific users
users: ["user_123", "user_456"],
// Minimum approvals required
minApprovals: 1
},
// Available actions
actions: [
{ id: "approve", label: "Approve & Publish", type: "approve" },
{ id: "edit", label: "Request Edits", type: "reject" },
{ id: "reject", label: "Reject", type: "reject" }
],
// Timeout handling
timeout: {
duration: "24h",
action: "escalate", // Options: "escalate", "auto-reject", "auto-approve"
escalateTo: ["senior-editor"]
},
// Notifications
notifications: {
channels: ["email", "slack", "push"],
onRequest: true,
onReminder: { after: "4h", repeat: "4h" },
onTimeout: true
}
}),
// Step 3: Publish (only if approved)
step({
id: "publish-content",
dependsOn: ["content-approval"],
condition: (ctx) => ctx.steps["content-approval"].output.action === "approve",
action: async (ctx) => {
const { content } = ctx.steps["generate-content"].output
const published = await cms.publish(content)
return { publishedId: published.id, url: published.url }
}
}),
// Alternative: Handle rejection
step({
id: "handle-rejection",
dependsOn: ["content-approval"],
condition: (ctx) => ctx.steps["content-approval"].output.action !== "approve",
action: async (ctx) => {
const { action, feedback } = ctx.steps["content-approval"].output
if (action === "edit") {
// Queue for re-generation with feedback
await queue.add("content-regeneration", {
originalContent: ctx.steps["generate-content"].output.content,
feedback
})
}
return { handled: true, action }
}
})
]
})hitl({
type: "approval",
actions: [
{ id: "approve", type: "approve" },
{ id: "reject", type: "reject" }
]
})hitl({
type: "review-edit",
editable: ["content", "subject", "recipients"],
actions: [
{ id: "approve", type: "approve" },
{ id: "approve-with-edits", type: "approve", requiresEdits: true },
{ id: "reject", type: "reject" }
]
})hitl({
type: "selection",
options: (ctx) => ctx.steps["generate-variants"].output.variants,
display: (option) => ({
title: option.name,
preview: option.content
}),
minSelections: 1,
maxSelections: 3
})hitl({
type: "input",
fields: [
{ name: "discountPercent", type: "number", min: 0, max: 50 },
{ name: "reason", type: "text", required: true },
{ name: "expiresAt", type: "date" }
],
actions: [
{ id: "submit", type: "approve" },
{ id: "cancel", type: "reject" }
]
})