Skip to content
Blog

Human-in-the-Loop Patterns for Autonomous Agent Workflows

Build approval gates into autonomous agents. Learn the approval lifecycle, dynamic policy, and pause-and-resume state with the OpenAI Agents SDK.

Published on August 20, 2026

AI Assistant

Autonomy is a sliding scale. Your agent can draft a refund email on its own. Issuing the refund itself—that’s a different conversation. The line between “helpful” and “dangerous” is drawn by human approval, and the hard part is doing it without breaking the agent’s flow.

The trick is to treat approval as a pause, not a failure. The agent runs, reaches a sensitive action, stops, waits for a decision—seconds or days—and resumes exactly where it left off. That’s the human-in-the-loop (HITL) pattern, and the OpenAI Agents SDK builds it into the run loop.

In this tutorial, you will learn the approval lifecycle, how to gate tools dynamically, and how to pause-and-resume across restarts.

Prerequisites

  • Python 3.9+ with pip install openai-agents
  • An OpenAI API key (or another provider)
  • A multi-agent workflow to apply approvals to (see our delegation tutorial)

The Approval Lifecycle

When a tool call needs review, the SDK follows the same pattern every time:

  1. The run records an approval interruption instead of executing the tool.
  2. The result returns interruptions plus a resumable state.
  3. Your application approves or rejects the pending items.
  4. You resume the same run from statenot a new user turn.
from agents import Agent, Runner, function_tool

@function_tool(needs_approval=True)
def delete_repo(repo_name: str) -> str:
    return git_service.delete(repo_name)

agent = Agent(
    name="DevOps agent",
    instructions="Help the user manage repositories.",
    tools=[delete_repo],
)

result = await Runner.run(agent, "Delete the repo 'legacy-monolith'")

if result.interruptions:
    interruption = result.interruptions[0]
    print("Waiting for approval:", interruption)

    decision = await prompt_user("Approve? (y/n): ")
    # Resume the SAME run with the human's decision.
    result = await Runner.run(
        agent,
        state=result.state,
        resume=decision,
    )

The run never restarts. Turn counts, history, and continuation IDs stay consistent because approval is a paused run, not a new turn.

Dynamic Approval Policy

Approving every tool call defeats the purpose of an agent. The needs_approval parameter accepts a callable, so policy can be dynamic:

@function_tool(
    needs_approval=lambda ctx, args, agent: float(args["amount"]) > 5000,
)
def issue_refund(order_id: str, amount: float) -> str:
    return refund_api.refund(order_id, amount)

Small refunds flow through automatically; large ones pause for a human. The same pattern gates on roles, environments, customer tiers, or any context you have access to.

Approvals Deep in the Workflow

Approvals work anywhere in the workflow—including after a handoff or inside a nested agent.as_tool() call. The model can still decide that an action is needed, but the run pauses until you approve or reject it:

triage_agent = Agent(
    name="Triage agent",
    instructions="Route to the right specialist. Hand off for payments.",
    handoffs=[payments_agent],   # payments_agent has needs_approval tools
)

The interruption pattern is identical whether the approving tool lives in the first agent, a handoff target, or a nested manager call.

Approve, Reject, or Edit

Resume values aren’t limited to approve/reject. Because resume accepts any value, a reviewer can return an edited draft, supply missing context, or inject computed results:

result = await Runner.run(agent, state=result.state, resume={
    "decision": "edit",
    "approved_text": draft_text_edited_by_human,
})

This is the pattern behind “edit before send,” where the human doesn’t just say yes—they correct the output before it ships.

Pausing Across Restarts

If review might take time—or the process dies—serialize the state and resume later. That’s still the same run:

import json

# Store the serialized state when pausing.
stored = json.dumps(result.state)

# Later, from any process:
state = json.loads(stored)
result = await Runner.run(agent, state=state, resume=decision)

The same state model serves streaming and delayed review. If a streamed run pauses, wait for it to settle, inspect interruptions, resolve the approvals, and resume from the same state.

Guardrails vs. Approvals

Guardrails and approvals work together to define whether a run continues, pauses, or stops:

  • Guardrails block automatically — for deterministic checks like off-topic input, secrets in tool args, or invalid output structure.
  • Approvals pause for judgment — for actions where a person or policy should weigh in, like refunds, deletions, or shell commands.
from agents import tool_input_guardrail, ToolGuardrailFunctionOutput

@tool_input_guardrail
def block_unknown_repos(data):
    args = json.loads(data.context.tool_arguments or "{}")
    if "legacy-monolith" in json.dumps(args):
        return ToolGuardrailFunctionOutput.reject_content(
            "This repo is protected. Route to owner approval."
        )
    return ToolGuardrailFunctionOutput.allow()

@tool(
    tool_input_guardrails=[block_unknown_repos],
    needs_approval=True,
)
def delete_repo(repo_name: str) -> str: ...

Guardrails screen what the agent is allowed to attempt; approvals gate whether the action executes.

The HITL Landscape Beyond OpenAI

The pause-and-resume mental model is universal:

  • LangGraph / Deep Agents: interrupt() pauses execution and surfaces a payload; Command(resume=...) continues it with the human’s response. Dynamic interrupts can be placed anywhere in code, wrapped in conditionals, or embedded inside tools.
  • Microsoft Agent Framework: human-in-the-loop middleware and checkpointing give the same pause/resume guarantees in graph workflows.

The principle is identical everywhere: the run must truly stop — free resources, release workers — then pick up later exactly where it left off.

Putting It All Together

For complete, runnable approval examples, see:

Conclusion & Next Steps

You now know how to add judgment to autonomous agents: gate tools with approvals, make policy dynamic, approve/reject/edit, and resume the same run across restarts.

Next steps:

  • Add approval gates to refund, delete, and shell tools with dynamic thresholds.
  • Build an “edit before send” flow that returns corrected drafts from resume.
  • Pair guardrails with approvals so deterministic checks never reach a human.

The most useful agents aren’t the most autonomous—they’re the ones that know when to ask. Human-in-the-loop is how you get the best of both.

References