← Blog

How we added human-in-the-loop approval to OpenAI's customer service agent

May 14, 2026 · 5 min read

The problem

OpenAI recently open-sourced a customer service agent demo built on their Agents SDK. It's a multi-agent system: a Triage Agent routes requests to specialist sub-agents — one for bookings and cancellations, one for seat changes, one for refunds, one for FAQs.

It works beautifully. A customer types “I need to cancel my flight.” The Triage Agent hands off to the Booking and Cancellation Agent. The agent confirms the details and calls cancel_flight. Flight cancelled. Two seconds, zero human involvement.

That's the problem.

There's nothing between the agent's decision and the action. No policy layer. No approval gate. No audit trail. For a demo, that's fine. For production — where real customers, real money, and real consequences are involved — it's not.

This is what we built tollgate to solve.

What we're integrating

The OpenAI CS agents demo is at github.com/openai/openai-cs-agents-demo. Python backend, Next.js frontend, runs locally in two terminal tabs. The agents handle cancellations, seat changes, flight status, and FAQs for a fictional airline.

Our goal: intercept the cancel_flight action before it executes, route it to a human for approval in Slack, and only proceed once someone clicks Approve. Zero changes to the agent's logic.

Step 1 — Clone the demo and install tollgate

bash
git clone https://github.com/openai/openai-cs-agents-demo
cd openai-cs-agents-demo/python-backend
pip install -r requirements.txt

You'll also need your tollgate API key. Sign up at usetollgate.com, create an agent, and copy the key — it looks like tg_live_....

Step 2 — Add the tollgate guard function

Open python-backend/airline/tools.py and add this helper function:

python
import asyncio
import httpx
import os

TOLLGATE_BASE_URL = os.environ.get("TOLLGATE_BASE_URL", "https://toll-gate-production.up.railway.app")
TOLLGATE_API_KEY = os.environ.get("TOLLGATE_API_KEY", "")

async def tollgate_guard(action: str, payload: dict) -> str:
    """
    Submit an action to tollgate for policy evaluation.
    Returns 'allowed' if approved, 'denied' if rejected.
    Blocks and polls if the decision is pending (requires human approval).
    Times out after 5 minutes and defaults to deny.
    """
    import uuid
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"{TOLLGATE_BASE_URL}/v1/check",
            json={
                "action_name": action,
                "idempotency_key": str(uuid.uuid4()),
                "payload": payload,
            },
            headers={"Authorization": f"Bearer {TOLLGATE_API_KEY}"},
            timeout=10.0,
        )
        resp.raise_for_status()
        result = resp.json()
        decision = result["decision"]
        if decision in ("allowed", "denied"):
            return decision
        action_id = result["action_id"]
        for _ in range(150):  # poll up to 5 minutes
            await asyncio.sleep(2)
            poll = await client.get(
                f"{TOLLGATE_BASE_URL}/v1/check/{action_id}",
                headers={"Authorization": f"Bearer {TOLLGATE_API_KEY}"},
                timeout=10.0,
            )
            poll.raise_for_status()
            status = poll.json()["decision"]
            if status in ("allowed", "approved"):
                return "allowed"
            if status in ("denied", "rejected"):
                return "denied"
        return "denied"  # timeout = deny for safety

Step 3 — Wrap the cancel_flight action

Find the cancel_flight function in the same file and add the tollgate check before the return statement:

python
@function_tool(
    name_override="cancel_flight",
    description_override="Cancel a flight."
)
async def cancel_flight(
    context: RunContextWrapper[AirlineAgentChatContext]
) -> str:
    """Cancel the flight in the context."""
    apply_itinerary_defaults(context.context.state)
    fn = context.context.state.flight_number
    assert fn is not None, "Flight number is required"
    confirmation = context.context.state.confirmation_number or "".join(
        random.choices(string.ascii_uppercase + string.digits, k=6)
    )
    context.context.state.confirmation_number = confirmation

    # tollgate policy check — intercepts before execution
    decision = await tollgate_guard(
        action="cancel_flight",
        payload={
            "flight_number": fn,
            "confirmation_number": confirmation,
            "passenger_name": getattr(context.context.state, "passenger_name", None),
        }
    )

    if decision != "allowed":
        return f"Cancellation of flight {fn} requires human approval or was denied."

    return f"Flight {fn} successfully cancelled for confirmation {confirmation}"

That's the entire integration. The agent code is otherwise untouched.

Step 4 — Define the policy

In your tollgate dashboard, navigate to your agent and open the Policy tab. Add this rule:

yaml
version: 1
rules:
  - action: cancel_flight
    decide: require_approval
    approvers: ["#approvals"]
default: allow

This tells tollgate: whenever cancel_flight is called, pause execution and route to the #approvals Slack channel for human sign-off. Everything else auto-allows.

Step 5 — Connect Slack and run

In tollgate dashboard Settings, connect your Slack workspace. Then start the backend with your keys:

bash
export OPENAI_API_KEY=your-openai-key
export TOLLGATE_API_KEY=tg_live_your-key
python3 -m uvicorn main:app --port 8001

Now try it. Type “I need to cancel my flight” in the demo UI. The agent processes the request, verifies the booking — and then pauses. A message appears in your Slack #approvals channel:

# approvals

T
TollgateToday at 2:41 PM

🔔 Approval Required

Agentairline-demo-agent
Actioncancel_flight
Payload{"flight_number": "PA441", "passenger_name": "Morgan Lee", "confirmation_number": "IR-D204"}
Rulematched rule for cancel_flight
✅ Approve❌ Reject

Click Approve. The agent receives the decision, and the cancellation completes. The customer sees: “Your flight PA441 has been successfully cancelled.”

Click Reject. The agent tells the customer the cancellation couldn't be processed and suggests contacting support.

What just happened

The agent called cancel_flight. Tollgate intercepted it before execution, evaluated the policy, found a matching rule, and held the action pending human approval. The agent's execution was paused — blocking on the tollgate_guard poll — until a human made a decision in Slack. Then execution resumed.

The agent never knew about Slack. The agent never knew about the policy. The agent just called a function and got back a result. Everything else happened in the tollgate layer.

The pattern

One function. One policy file. Zero changes to agent logic.

This is the tollgate integration pattern. It works the same way for any agent action — refunds, account deletions, deployments, wire transfers. Wrap the function, define the policy, connect your approval channel.

Your agents can act. Safely.

Get started

Sign up free at usetollgate.com. The Python SDK installs in one line, the first policy takes five minutes to write, and your first Slack approval fires the same day.

If you build something with tollgate, we'd love to hear about it — [email protected]