Human-in-the-Loop
Pause agents for approval before executing sensitive tools.
Human-in-the-Loop solves three problems that appear when agents move from answering questions to taking actions:
-
Irreversible operations. Sending an email, deleting a record, or posting to a channel cannot be undone. A human checkpoint prevents mistakes that require cleanup or apologies.
-
Missing context. The agent knows what action to take but lacks a critical detail. A deployment needs a target environment. A booking needs a budget. Rather than guessing, the agent pauses and asks.
-
Audit trail. Sensitive operations need accountability. Slack threads already contain the discussion that led to the action. Rendering the approval in the same thread keeps the decision and its context together.
Quick start
The Slack interface renders ordinary HITL pauses as interactive cards in the thread. Users can confirm, deny, or provide input. These buttons are not an admin-role authorization system; required @approval records use the separate approval flow.
Follow the Slack setup guide, including interactivity URLs and signing credentials. Install uv pip install -U "agno[os,slack]" openai and set OPENAI_API_KEY in the server terminal. Save one tab as its named file and run it, for example python agent.py. The email tool below is a simulation.
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS
from agno.os.interfaces.slack import Slack
from agno.tools import tool
@tool(requires_confirmation=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Simulate an email after confirmation."""
return f"Simulation: email to {to} with subject {subject}; nothing was sent."
db = SqliteDb(db_file="agent.db")
agent = Agent(
name="Assistant",
model=OpenAIResponses(id="gpt-5.4"),
tools=[send_email],
db=db,
)
agent_os = AgentOS(
agents=[agent],
db=db,
interfaces=[Slack(agent=agent)],
)
app = agent_os.get_app()
if __name__ == "__main__":
agent_os.serve(app="agent:app", reload=True)from agno.agent import Agent
from agno.team import Team
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS
from agno.os.interfaces.slack import Slack
from agno.tools import tool
@tool(requires_confirmation=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Simulate an email after confirmation."""
return f"Simulation: email to {to} with subject {subject}; nothing was sent."
db = SqliteDb(db_file="agent.db")
researcher = Agent(name="Researcher", model=OpenAIResponses(id="gpt-5.4"))
writer = Agent(name="Writer", model=OpenAIResponses(id="gpt-5.4"), tools=[send_email])
support_team = Team(
name="Support Team",
mode="coordinate",
members=[researcher, writer],
model=OpenAIResponses(id="gpt-5.4"),
db=db,
)
agent_os = AgentOS(
teams=[support_team],
db=db,
interfaces=[Slack(team=support_team)],
)
app = agent_os.get_app()
if __name__ == "__main__":
agent_os.serve(app="team:app", reload=True)When a member agent calls a tool that requires confirmation, the team pauses.
from agno.agent import Agent
from agno.workflow import Step, Workflow
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS
from agno.os.interfaces.slack import Slack
from agno.tools import tool
@tool(requires_confirmation=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Simulate an email after confirmation."""
return f"Simulation: email to {to} with subject {subject}; nothing was sent."
db = SqliteDb(db_file="agent.db")
researcher = Agent(name="Researcher", model=OpenAIResponses(id="gpt-5.4"))
writer = Agent(name="Writer", model=OpenAIResponses(id="gpt-5.4"), tools=[send_email])
research_flow = Workflow(
name="Research",
steps=[
Step(name="Research", agent=researcher),
Step(name="Write", agent=writer),
],
db=db,
)
agent_os = AgentOS(
workflows=[research_flow],
db=db,
interfaces=[Slack(workflow=research_flow)],
)
app = agent_os.get_app()
if __name__ == "__main__":
agent_os.serve(app="workflow:app", reload=True)Workflow step pauses surface in Slack the same way.
HITL requires a database to persist paused runs.
Pause types
| Pause type | Slack card | Trigger |
|---|---|---|
| Confirmation | Approve/Deny buttons | @tool(requires_confirmation=True) |
| User input | Text fields or dropdowns | @tool(requires_user_input=True) |
| External execution | Result field, submitted back to the run | @tool(external_execution=True) |
| User feedback | Question forms with checkboxes or dropdowns | UserFeedbackTools() |
See the HITL overview for details on each pause type.