Skip to content

Human in the Loop

The HumanInTheLoop intervention handler pauses agent execution before tool calls to request human approval. It provides a configurable, drop-in way to add human oversight without writing custom interrupt logic. Pass it to interventions and choose how you want to collect the human’s response.

flowchart LR
A[Tool call] --> B{Allowed?}
B -->|Yes| C[Execute]
B -->|No| D{Trusted?}
D -->|Yes| C
D -->|No| E{Human approves?}
E -->|Yes| C
E -->|No| F[Cancel]

The handler uses the confirm action to pause for human input. Under the hood it builds on the SDK’s interrupt mechanism, but abstracts away the manual interrupt/resume loop when you provide an ask option.

Without an ask option, the handler raises an interrupt and the agent pauses. The caller presents the interrupt to the user, collects their response, and resumes the agent. This is the same interrupt/resume pattern used throughout the SDK. For stateless deployments, combine with a session manager to persist state between requests.

import { Agent, tool, InterruptResponseContent } from '@strands-agents/sdk'
import { HumanInTheLoop } from '@strands-agents/sdk/vended-interventions/hitl'
import { z } from 'zod'
const deleteFiles = tool({
name: 'delete_files',
description: 'Delete files at the given paths',
inputSchema: z.object({ paths: z.array(z.string()) }),
callback: (input) => `Deleted ${input.paths.length} files`,
})
const agent = new Agent({
tools: [deleteFiles],
interventions: [new HumanInTheLoop()],
})
// Agent pauses with stopReason 'interrupt' when a tool needs approval
let result = await agent.invoke('Delete the temp files')
if (result.stopReason === 'interrupt') {
// Present the interrupt to the user (web UI, Slack, etc.)
console.log(result.interrupts![0].reason)
// Resume with the human's response
result = await agent.invoke([
new InterruptResponseContent({
interruptId: result.interrupts![0].id,
response: 'yes', // 'y', 'yes', or true → approved
}),
])
}
console.log('Result:', result.lastMessage)

For CLI applications, pass ask: 'stdio' to prompt the user inline via Node.js readline. The agent blocks until the user responds, so no interrupt handling is needed on the caller side.

import { Agent, tool } from '@strands-agents/sdk'
import { HumanInTheLoop } from '@strands-agents/sdk/vended-interventions/hitl'
import { z } from 'zod'
// const deleteFiles = tool({ ... }) — same as above
const agent = new Agent({
tools: [deleteFiles],
interventions: [new HumanInTheLoop({ ask: 'stdio' })],
})
await agent.invoke('Delete the temp files')
// Terminal prompt:
// Tool "delete_files" requires human approval. Input: {...} (y/n):

For web UIs, Slack bots, or other custom interfaces, pass an async function to ask. The function receives a prompt string describing the tool call and must return the user’s response.

import { Agent, tool } from '@strands-agents/sdk'
import { HumanInTheLoop } from '@strands-agents/sdk/vended-interventions/hitl'
import { z } from 'zod'
// const deleteFiles = tool({ ... }) — same as above
const agent = new Agent({
tools: [deleteFiles],
interventions: [
new HumanInTheLoop({
ask: async (prompt) => {
// Your UI: Slack DM, web modal, push notification, etc.
return await askUserViaSlack(prompt)
},
}),
],
})
await agent.invoke('Delete the temp files')

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | allowedTools | string[] | undefined | Tools that bypass approval. Supports '*' (all) and '!toolName' (negation). | | enableTrust | boolean | false | When true, trust responses are remembered for the session. | | evaluateTrust | Function | Accepts 't' or 'trust' | Custom validator for trust responses. Only evaluated when enableTrust is true. | | evaluate | Function | Accepts true, 'y', or 'yes' | Custom validator for approval responses. | | ask | Function or 'stdio' | undefined | Pass an async function for custom UIs, 'stdio' for CLI readline, or omit for interrupt/resume. |

Use allowedTools to skip approval for safe, read-only tools:

import { Agent, tool } from '@strands-agents/sdk'
import { HumanInTheLoop } from '@strands-agents/sdk/vended-interventions/hitl'
import { z } from 'zod'
// const deleteFiles = tool({ ... }) — same as above
// const readFile = tool({ ... })
const agent = new Agent({
tools: [readFile, deleteFiles],
interventions: [
new HumanInTheLoop({
ask: 'stdio',
// Pattern syntax:
// 'read_file' → runs without approval
// '*' → all tools run freely (disables handler)
// ['*', '!delete_files'] → all except delete_files
allowedTools: ['read_file'],
}),
],
})
await agent.invoke('Read config.json then delete /tmp/old-logs')
// Only delete_files prompts; read_file executes immediately

When enableTrust is true, a human can respond with 't' or 'trust' to approve the current tool call AND remember that decision for the rest of the session. Subsequent calls to the same tool skip the prompt entirely. Trust works in all modes: interrupt/resume, stdio, and custom callbacks.

Trust state is stored in agent.appState and persists across turns within a session but resets when the agent is re-created. Negated tools ('!toolName') cannot be trusted and always prompt.

import { Agent, tool } from '@strands-agents/sdk'
import { HumanInTheLoop } from '@strands-agents/sdk/vended-interventions/hitl'
import { z } from 'zod'
// const deleteFiles = tool({ ... }) — same as above
const agent = new Agent({
tools: [deleteFiles],
interventions: [
new HumanInTheLoop({
ask: 'stdio',
enableTrust: true,
}),
],
})
await agent.invoke('Delete all log files in /tmp')
// First call: user responds 't' → approved AND remembered
// Subsequent calls: no prompt needed for the session

By default, the handler accepts true, 'y', or 'yes' as approval. Use evaluate to define your own approval logic, for example requiring a one-time passcode:

import { Agent, tool } from '@strands-agents/sdk'
import { HumanInTheLoop } from '@strands-agents/sdk/vended-interventions/hitl'
import { z } from 'zod'
const transferFunds = tool({
name: 'transfer_funds',
description: 'Transfer funds between accounts',
inputSchema: z.object({
from: z.string(),
to: z.string(),
amount: z.number(),
}),
callback: (input) =>
`Transferred $${input.amount} from ${input.from} to ${input.to}`,
})
const expectedOtp = '483291'
const agent = new Agent({
tools: [transferFunds],
interventions: [
new HumanInTheLoop({
ask: async (prompt) => {
await sendOtpToUser(expectedOtp)
return await collectUserInput(
prompt + ' Enter OTP to confirm:'
)
},
// Only approve if the user enters the correct OTP
evaluate: (response) => response === expectedOtp,
}),
],
})
await agent.invoke('Transfer $500 from checking to savings')
  • Setup: interventions: [new HumanInTheLoop()] vs. writing a custom hook class + resume loop
  • Inline collection: Built-in (ask: 'stdio' or callback) vs. implementing yourself
  • Trust/remember: Built-in (enableTrust) vs. manual appState logic
  • Tool allow-list: Built-in (allowedTools) vs. custom conditional logic
  • Flexibility: HITL is opinionated for approval workflows; raw interrupts give full control over any interrupt shape

Use HumanInTheLoop when you want standard approval gating with minimal code. Use raw interrupts when you need custom interrupt shapes, multi-step interactions, or non-approval workflows.