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.

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.

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]

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
}),
])
}

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')
ParameterTypeDefaultDescription
allowedToolsstring[]undefinedTools that bypass approval. Supports '*' (all) and '!toolName' (negation).
enableTrustbooleanfalseWhen true, trust responses are remembered for the session.
evaluateTrustFunctionAccepts 't' or 'trust'Custom validator for trust responses. Only evaluated when enableTrust is true.
evaluateFunctionAccepts true, 'y', or 'yes'Custom validator for approval responses.
askFunction or 'stdio'undefinedPass 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 the user to type “confirm”:

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',
// Only approve if the user types "confirm"
evaluate: (response) =>
typeof response === 'string' && response.toLowerCase() === 'confirm',
}),
],
})
await agent.invoke('Delete the temp files')
// Prompt: Tool "delete_files" requires human approval. Input: {...}
// User must type "confirm" to approve (not just "y" or "yes")

Use HumanInTheLoop when you want tool-level approval gating with minimal code: it handles allow-lists, trust, and collection mode out of the box. Use raw interrupts when you need full control: custom interrupt shapes, multi-step interactions, or workflows beyond simple approve/deny.