Built-in - Framework-level agent. Configure only - cannot modify source.
Overview
The HITL (Human-in-the-Loop) agent suspends workflow execution for manual approval. It leverages Cloudflare Durable Objects for stateful, reliable state persistence across workflow suspensions and resumptions, enabling truly durable human-in-the-loop workflows that can pause for hours or days without losing state.
Features:
Durable Object storage for reliable state persistence across suspensions
Stateful workflow resumption - workflows pause and resume seamlessly
Multiple notifications : Slack, Email (via MailChannels), Webhook
Configurable timeouts with auto-expiry
Approval/rejection with comments
Required Bindings
Add this to your wrangler.toml:
# Required: Durable Object for HITL state
[[ durable_objects . bindings ]]
name = "HITL_STATE"
class_name = "HITLState"
[[ migrations ]]
tag = "v1"
new_classes = [ "HITLState" ]
Actions
The HITL agent supports two primary actions for controlling workflow execution:
suspend
Pauses workflow execution and waits for human approval. The workflow state is persisted to a Durable Object, allowing it to resume from the exact same point later.
agents :
- name : await-approval
agent : hitl
config :
action : suspend
timeout : 86400000 # 24 hours in ms
notificationChannel : slack
notificationConfig :
webhookUrl : "$env.SLACK_WEBHOOK_URL"
baseUrl : "https://my-worker.workers.dev"
input :
approvalData :
content : ${generate-content.output}
author : ${input.author}
resume
Resumes a suspended workflow with approval/rejection data. This is typically called via the callback URL mechanism.
# Resume happens automatically via callback POST
POST /callback/:token
Body : {
approved : true/false ,
feedback? : string ,
reason? : string ,
approver? : string
}
Durable Objects Integration
The HITL agent uses Cloudflare Durable Objects to provide stateful human-in-the-loop workflows:
Why Durable Objects?
State persistence - Workflow state survives across Worker invocations
Global consistency - Each workflow gets a unique Durable Object instance
Long-lived workflows - Support for approvals that take hours or days
Automatic cleanup - State is removed after approval/rejection or timeout
How it works:
When suspend is called, workflow state is serialized to a Durable Object
A unique token is generated for resumption
Notifications are sent with callback URLs containing the token
When the callback URL is invoked, the Durable Object state is retrieved
Workflow resumes from the exact point where it was suspended
State is cleaned up after resumption or timeout
Basic Usage
agents :
- name : generate-content
operation : think
config :
prompt : Write a blog post about ${input.topic}
- name : request-approval
agent : hitl
inputs :
data : ${generate-content.output}
prompt : "Review this blog post"
approvers : [ $ { env.ADMIN_EMAIL }]
- name : publish
condition : ${request-approval.output.approved}
operation : http
config :
url : https://api.example.com/publish
body : ${generate-content.output}
inputs :
data :
type : any
required : true
description : Data to review
prompt :
type : string
required : true
description : Review instructions
approvers :
type : array
required : true
description : Email addresses of approvers
timeout :
type : number
default : 86400 # 24 hours
description : Approval timeout (seconds)
minApprovals :
type : number
default : 1
description : Minimum approvals required
metadata :
type : object
description : Additional context
Configuration
Single Approver
agents :
- name : approve
agent : hitl
inputs :
data : ${previous.output}
prompt : "Review this change"
approvers : [ admin@example.com ]
Multiple Approvers
agents :
- name : approve
agent : hitl
inputs :
data : ${previous.output}
prompt : "Review this change"
approvers :
- manager@example.com
- director@example.com
minApprovals : 2 # Both must approve
With Timeout
agents :
- name : approve
agent : hitl
inputs :
data : ${previous.output}
prompt : "Review within 1 hour"
approvers : [ admin@example.com ]
timeout : 3600 # 1 hour
- name : handle-timeout
condition : ${approve.output.status === 'timeout'}
operation : email
config :
to : ${env.ESCALATION_EMAIL}
subject : "Approval timeout"
agents :
- name : approve
agent : hitl
inputs :
data : ${previous.output}
prompt : "Review this transaction"
approvers : [ finance@example.com ]
metadata :
amount : ${previous.output.amount}
customer : ${input.customer_name}
risk_score : ${calculate-risk.output.score}
Complete Workflows
Content Approval
ensemble : content-approval
agents :
# 1. Generate content
- name : generate
operation : think
config :
prompt : Write about ${input.topic}
# 2. Request approval
- name : approve
agent : hitl
inputs :
data : ${generate.output}
prompt : |
Review this content for:
- Accuracy
- Tone
- Brand guidelines
approvers :
- editor@example.com
- marketing@example.com
minApprovals : 2
# 3. Publish if approved
- name : publish
condition : ${approve.output.approved}
operation : http
config :
url : https://cms.example.com/publish
body : ${generate.output}
# 4. Notify rejection
- name : notify-rejection
condition : ${approve.output.rejected}
operation : email
config :
to : ${env.CONTENT_TEAM}
subject : "Content rejected"
body : |
Reason: ${approve.output.reason}
Feedback: ${approve.output.feedback}
output :
status : ${approve.output.status}
published : ${approve.output.approved}
Transaction Approval
ensemble : transaction-approval
agents :
# 1. Calculate risk
- name : risk-check
operation : think
config :
prompt : Assess risk for ${input.transaction}
# 2. Auto-approve low risk
- name : auto-approve
condition : ${risk-check.output.risk_score < 0.3}
operation : code
config :
script : scripts/auto-approve
input :
riskScore : ${risk-check.output.risk_score}
# 3. Request approval for high risk
- name : manual-review
condition : ${risk-check.output.risk_score >= 0.3}
agent : hitl
inputs :
data : ${input.transaction}
prompt : "High-risk transaction - manual review required"
approvers :
- fraud@example.com
- senior-manager@example.com
metadata :
risk_score : ${risk-check.output.risk_score}
amount : ${input.transaction.amount}
customer : ${input.transaction.customer}
# 4. Process if approved
- name : process
condition : ${auto-approve.output.approved || manual-review.output.approved}
operation : http
config :
url : https://payment-api.example.com/process
body : ${input.transaction}
// scripts/auto-approve.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'
export default function autoApprove ( context : AgentExecutionContext ) {
const { riskScore } = context . input
return { approved: true , auto: true }
}
Code Review
ensemble : code-review
agents :
# 1. Run automated checks
- name : lint
operation : code
config :
script : scripts/run-linter
input :
code : ${input.diff}
- name : test
operation : code
config :
script : scripts/run-tests
input :
code : ${input.diff}
# 2. Request human review
- name : review
condition : ${lint.output.passed && test.output.passed}
agent : hitl
inputs :
data :
diff : ${input.diff}
lint_results : ${lint.output}
test_results : ${test.output}
prompt : |
Review this code change:
- Code quality
- Test coverage
- Security concerns
approvers :
- tech-lead@example.com
- senior-dev@example.com
minApprovals : 1
# 3. Merge if approved
- name : merge
condition : ${review.output.approved}
operation : http
config :
url : https://api.github.com/repos/${input.repo}/pulls/${input.pr_number}/merge
method : PUT
// scripts/run-linter.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'
export default function runLinter ( context : AgentExecutionContext ) {
const { code } = context . input
// Run linter on the code
return { passed: true }
}
// scripts/run-tests.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'
export default function runTests ( context : AgentExecutionContext ) {
const { code } = context . input
// Run tests on the code
return { passed: true }
}
Output Schema
{
status : 'pending' | 'approved' | 'rejected' | 'timeout' ;
approved : boolean ;
rejected : boolean ;
approvals : Array <{
approver : string ;
approved : boolean ;
timestamp : string ;
feedback ?: string ;
}>;
reason ?: string ; // Rejection reason
feedback ?: string ; // Approver feedback
timedOut : boolean ;
}
Notification Channels
The HITL agent supports three notification channels to alert approvers:
Slack Notifications
agents :
- name : await-approval
agent : hitl
config :
action : suspend
timeout : 86400000 # 24 hours in ms
notificationChannel : slack
notificationConfig :
webhookUrl : "$env.SLACK_WEBHOOK_URL"
baseUrl : "https://my-worker.workers.dev"
input :
approvalData :
content : ${generate-content.output}
author : ${input.author}
Slack notifications include:
Formatted approval request with emoji
Execution ID and approval data
Approve/Reject buttons as URLs
Expiration countdown
Email Notifications (via MailChannels)
agents :
- name : await-approval
agent : hitl
config :
action : suspend
notificationChannel : email
notificationConfig :
to : "approver@example.com"
from : "noreply@yourdomain.com"
subject : "Approval Required" # Optional
baseUrl : "https://my-worker.workers.dev"
input :
approvalData :
content : ${generate-content.output}
Email notifications use the MailChannels API which is free for Cloudflare Workers. The sender email must be from a domain you control.
Webhook Notifications
agents :
- name : await-approval
agent : hitl
config :
action : suspend
notificationChannel : webhook
notificationConfig :
webhookUrl : "https://your-system.com/approval-webhook"
baseUrl : "https://my-worker.workers.dev"
input :
approvalData :
content : ${generate-content.output}
Webhook payload:
{
"executionId" : "hitl_abc123..." ,
"approvalData" : { ... },
"callbackUrl" : "https://my-worker.workers.dev/callback/hitl_abc123" ,
"approveUrl" : "https://my-worker.workers.dev/callback/hitl_abc123?action=approve" ,
"rejectUrl" : "https://my-worker.workers.dev/callback/hitl_abc123?action=reject" ,
"expiresAt" : 1732924800000
}
Callback URLs
When a workflow is suspended for approval, Conductor generates a unique resumption token. The approval/rejection is handled via callback URLs:
# Resume with approval/rejection data
POST /callback/:token
Body: { approved: true/false, feedback?: string, reason?: string }
# Get token metadata (without consuming it)
GET /callback/:token
The base path (/callback) is configurable via APIConfig.hitl.resumeBasePath.
Security Model
The callback URLs use token-based authentication - the token itself IS the auth (like a password reset link):
Token is cryptographically generated (crypto.randomUUID())
Token is one-time use (deleted after resumption)
Token has expiration (configured via timeout)
Token is delivered via secure channel (notification to authorized user)
Example: Custom Approval UI
// Get approval context
const response = await fetch ( `https://your-worker.workers.dev/callback/ ${ token } ` );
const { metadata } = await response . json ();
// Display approval UI with metadata...
// User clicks approve
await fetch ( `https://your-worker.workers.dev/callback/ ${ token } ` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
approved: true ,
feedback: 'Looks good!' ,
approver: 'jane@example.com'
})
});
// Or user clicks reject
await fetch ( `https://your-worker.workers.dev/callback/ ${ token } ` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
approved: false ,
reason: 'Needs revision' ,
feedback: 'Please fix the formatting'
})
});
Best Practices
1. Set Reasonable Timeouts
timeout : 86400 # 24 hours for content review
timeout : 3600 # 1 hour for urgent approvals
timeout : 604800 # 1 week for major decisions
2. Handle Timeouts Gracefully
agents :
- name : approve
agent : hitl
inputs :
timeout : 3600
- name : escalate
condition : ${approve.output.status === 'timeout'}
operation : email
config :
to : ${env.ESCALATION_EMAIL}
3. Provide Context
inputs :
prompt : |
Review this transaction for:
- Fraud indicators
- Risk score > 0.7
- Amount > $10,000
metadata :
risk_factors : ${risk-check.output.factors}
customer_history : ${customer-history.output}
4. Use Appropriate Approver Counts
# Single approver for routine
minApprovals : 1
# Multiple for critical
minApprovals : 2
# Majority for committees
minApprovals : ${Math.ceil(approvers.length / 2)}
5. Track Rejections
agents :
- name : approve
agent : hitl
- name : log-rejection
condition : ${approve.output.rejected}
operation : data
config :
backend : d1
binding : DB
operation : execute
sql : INSERT INTO rejections (data, reason, timestamp) VALUES (?, ?, ?)
params :
- ${approve.input.data}
- ${approve.output.reason}
- ${now()}
Common Use Cases
Expense Approval
agents :
- name : approve-expense
agent : hitl
inputs :
data :
amount : ${input.amount}
category : ${input.category}
receipt : ${input.receipt_url}
prompt : "Approve this expense"
approvers : [ $ { input.manager_email }]
timeout : 172800 # 48 hours
Marketing Campaign
agents :
- name : approve-campaign
agent : hitl
inputs :
data : ${generate-campaign.output}
prompt : "Review campaign before launch"
approvers :
- marketing-director@example.com
- legal@example.com
minApprovals : 2
Data Export
agents :
- name : approve-export
agent : hitl
inputs :
data :
query : ${input.query}
row_count : ${preview.output.count}
includes_pii : ${check-pii.output.detected}
prompt : "Approve data export"
approvers :
- data-governance@example.com
Limitations
Max timeout : 7 days (default: 24 hours)
Notification channels : Slack, Email, Webhook (no Teams yet)
No delegation : Approvers can’t delegate to others
Single state per token : Each suspension creates a unique token
Next Steps
RAG Agent Retrieval-augmented generation
Built-in Overview All built-in agents