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
git clone https://github.com/openai/openai-cs-agents-demo
cd openai-cs-agents-demo/python-backend
pip install -r requirements.txtYou'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:
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 safetyStep 3 — Wrap the cancel_flight action
Find the cancel_flight function in the same file and add the tollgate check before the return statement:
@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:
version: 1
rules:
- action: cancel_flight
decide: require_approval
approvers: ["#approvals"]
default: allowThis 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:
export OPENAI_API_KEY=your-openai-key
export TOLLGATE_API_KEY=tg_live_your-key
python3 -m uvicorn main:app --port 8001Now 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
🔔 Approval Required
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]