Skip to main content
Slack scopes: app_mentions:read, assistant:write, chat:write, im:history
hitl_confirmation.py
"""
Slack HITL — Confirmation
=========================

Billing ops agent that can cancel customer subscriptions. Cancellation is
irreversible, so the destructive tool is wrapped with
`@tool(requires_confirmation=True)` — Slack pauses with Approve / Deny
buttons before the cancellation runs. The agent also has read-only
lookup tools so it can show the customer's context before asking to
confirm.

Try in Slack:
  @bot cancel C-42's subscription — they've been asking all week, churn reason pricing

Slack scopes: app_mentions:read, assistant:write, chat:write, im:history
"""

from dataclasses import dataclass
from typing import Dict, List

from agno.agent import Agent
from agno.db.sqlite.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.os.app import AgentOS
from agno.os.interfaces.slack import Slack
from agno.tools import tool

# Stand-in billing data — replace with real Stripe / internal client


@dataclass
class Subscription:
    customer_id: str
    plan: str
    monthly_rate: float
    status: str
    seat_count: int


_FAKE_DB: Dict[str, Subscription] = {
    "C-42": Subscription("C-42", "Team", 399.0, "active", 12),
    "C-77": Subscription("C-77", "Enterprise", 2499.0, "active", 120),
    "C-91": Subscription("C-91", "Starter", 49.0, "past_due", 3),
}


# Read-only tools — no HITL needed, agent uses these to build context


@tool
def lookup_customer(customer_id: str) -> str:
    """Return the customer's current subscription summary.

    Args:
        customer_id: Customer identifier (e.g. "C-42").
    """
    sub = _FAKE_DB.get(customer_id)
    if not sub:
        return f"No record for {customer_id}."
    return (
        f"{sub.customer_id}: plan={sub.plan}, rate=${sub.monthly_rate}/mo, "
        f"status={sub.status}, seats={sub.seat_count}."
    )


@tool
def list_active_subscriptions() -> List[Dict[str, str]]:
    """Return every active subscription. Useful when the user refers to
    a customer by something other than their id."""
    return [
        {"customer_id": s.customer_id, "plan": s.plan, "status": s.status}
        for s in _FAKE_DB.values()
        if s.status == "active"
    ]


# Destructive tool — pauses for human approval


@tool(requires_confirmation=True)
def cancel_subscription(customer_id: str, reason: str) -> str:
    """Cancel a customer subscription. Irreversible — stops billing and
    revokes access at the end of the current cycle.

    Args:
        customer_id: Customer identifier (e.g. "C-42").
        reason: Short human-readable cancellation reason.
    """
    sub = _FAKE_DB.get(customer_id)
    if not sub:
        return f"No record for {customer_id} — nothing to cancel."
    sub.status = "cancelled"
    return f"Subscription for {customer_id} cancelled. Reason logged: {reason!r}."


# Agent + AgentOS + Slack interface

db = SqliteDb(
    db_file="tmp/hitl_confirmation.db",
    session_table="agent_sessions",
    approvals_table="approvals",
)

agent = Agent(
    name="Billing Ops Agent",
    id="billing-ops-agent",
    model=OpenAIResponses(id="gpt-5.4"),
    db=db,
    tools=[lookup_customer, list_active_subscriptions, cancel_subscription],
    instructions=[
        "You are a billing operations assistant embedded in Slack.",
        "Before calling cancel_subscription, use lookup_customer (or "
        "list_active_subscriptions if the user didn't give an id) so you can "
        "show plan + rate in your summary.",
        "When ready to cancel, call cancel_subscription with the customer_id "
        "and a short reason drawn from the user's message. Do NOT ask the user "
        "for final confirmation yourself — the Slack interface will pause the "
        "run and show an Approve / Deny card.",
    ],
    markdown=True,
)

agent_os = AgentOS(
    description="Slack HITL — confirmation (subscription cancellation)",
    agents=[agent],
    db=db,
    interfaces=[
        Slack(
            agent=agent,
            reply_to_mentions_only=True,
        ),
    ],
)
app = agent_os.get_app()


if __name__ == "__main__":
    agent_os.serve(app="hitl_confirmation:app", reload=True)

Run the Example

1

Set up your virtual environment

uv venv --python 3.12
source .venv/bin/activate
uv venv --python 3.12
.venv\Scripts\activate
2

Install dependencies

uv pip install -U "agno[os,slack]" fastmcp openai starlette
3

Export your API keys

export JWT_VERIFICATION_KEY="your_jwt_verification_key_here"
export OPENAI_API_KEY="your_openai_api_key_here"
$Env:JWT_VERIFICATION_KEY="your_jwt_verification_key_here"
$Env:OPENAI_API_KEY="your_openai_api_key_here"
4

Run the example

Save the code above as hitl_confirmation.py, then run:
python hitl_confirmation.py
Full source: cookbook/05_agent_os/interfaces/slack/hitl_confirmation.py