Use the OpenAI Agents SDK with FlowRunner
Code for sending OpenAI Agents SDK approvals to people in Slack or email through FlowRunner, resuming the run safely, and giving agents flows as tools.
Your agents stay in your app, in Python or TypeScript. This page wires them to FlowRunner: an approval goes to a person in Slack or by email and the run resumes when they answer, a flow serves as a function tool, and a business event starts the agent.
The FlowRunner side is built from a prompt with FlowRunner Prompt to Flow, so you don’t need to learn the flow editor. The Python samples read the workspace id and key from FLOWRUNNER_WORKSPACE_ID and FLOWRUNNER_KEY. Both are in Workspace Settings, under Credentials.
1. Connect FlowRunner Prompt to Flow
FlowRunner Prompt to Flow is an MCP server you add to the AI tool you already use. You connect it once and sign in with your FlowRunner account. The server URL is the same for every tool: https://app.flowrunner.ai/mcp.
- Claude (web and desktop): open Settings > Connectors, click + Add, then Add custom connector. Enter
FlowRunnerand the server URL, click Add, then Connect, and approve the FlowRunner sign-in. On a Team or Enterprise plan, an Owner adds it once under Organization settings > Connectors. - Claude Code: run these two commands in your project folder, and approve the sign-in page that opens:
claude mcp add --transport http flowrunner https://app.flowrunner.ai/mcp
claude mcp login flowrunner
- ChatGPT (on the web): open ChatGPT Plugins, click Add, then Add custom MCP server. Enter
FlowRunner, put the server URL under Connection, choose OAuth, and approve the sign-in. In a chat, type@and pick FlowRunner. - Codex: add the server to
~/.codex/config.toml, then runcodex mcp login flowrunner:
[mcp_servers.flowrunner]
url = "https://app.flowrunner.ai/mcp"
- Cursor: add the server to
~/.cursor/mcp.json, then open Customize > MCPs and follow the sign-in prompt:
{
"mcpServers": {
"flowrunner": { "url": "https://app.flowrunner.ai/mcp" }
}
}
If you don't have a FlowRunner account yet, the Free plan needs no card, and every new account starts with 14 days of Professional. The setup guide in the docs has each tool step by step.
Pattern: your agent asks a person, FlowRunner gets the answer
The SDK pauses the run before a tool that needs approval. Your app saves the paused state and hands the request to a FlowRunner flow. The flow reaches the approver, waits, and posts the decision back. Your app checks it and resumes.
Build the approval flow. Paste a request like this into your AI tool:
Using the FlowRunner tools, build a flow called “Agent Approval”. My app calls it through the API without waiting for an answer. It sends
requestId,summary, andcalls, a list where each item hastoolandarguments. Post the summary and the calls to #approvals in Slack with Approve and Reject buttons and a box for a reason. Wait for the answer, however long it takes. Then POST JSON tohttps://agents.example.com/approvalswithrequestId,approved(true or false),reason, and who answered. Send the headerX-Approval-Secreton that request, and leave its value empty: I’ll paste it into the HTTP Request block myself in the flow editor. Retry that call on 429 and 5XX up to 3 times with exponential backoff, and if it still fails, post the error in #approvals. Ask me before you make it live, and tell me exactly what my app sends and what my endpoint receives.
Swap Slack for email, WhatsApp, phone, or a Form if that’s where your approvers are. The AI builds the flow, sends you a link to connect Slack if it isn’t connected, and asks before making it live. It picks field names where the prompt doesn’t, so read them from its answer.
Pause and hand off. When the run stops on interruptions, save the state server-side under a new request id, then start the flow. The non-blocking call answers at once with an executionId:
import os
import uuid
import requests
from agents import Agent, Runner, RunState
from agents.decorators import tool
FR_BASE = f"https://api.flowrunner.ai/api/v1/{os.environ['FLOWRUNNER_WORKSPACE_ID']}"
FR_HEADERS = {"FR-Access-Key": os.environ["FLOWRUNNER_KEY"]}
@tool(needs_approval=True)
async def issue_refund(order_id: str, amount: float) -> str:
"""Refund an order."""
# One key per logical refund, so a replayed approval can't refund twice.
return payments.refund(order_id, amount, idempotency_key=f"refund-{order_id}")
agent = Agent(
name="Support agent",
instructions="Resolve tickets. Refunds need approval.",
tools=[issue_refund],
)
async def handle_ticket(ticket_text: str) -> str:
result = await Runner.run(agent, ticket_text)
if not result.interruptions:
return result.final_output
request_id = str(uuid.uuid4())
pending.save(request_id, result.to_state().to_json()) # your database, server-side
r = requests.post(
f"{FR_BASE}/flows/<flow-id>/activate",
headers=FR_HEADERS,
json={
"requestId": request_id,
"summary": ticket_text[:500],
"calls": [
{"tool": item.name, "arguments": item.arguments}
for item in result.interruptions
],
},
timeout=10,
)
r.raise_for_status()
return "Waiting for approval"
Resume when the answer arrives. The flow posts the decision to your endpoint. This is where the checks from OpenAI’s guide live: verify the caller, claim the request exactly once, then restore the state and apply the decision:
import hmac
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
@app.post("/approvals")
async def approval_answer(body: dict, x_approval_secret: str = Header(...)):
if not hmac.compare_digest(x_approval_secret, os.environ["APPROVAL_SECRET"]):
raise HTTPException(status_code=401)
stored = pending.claim(body["requestId"]) # atomic: returns None if already claimed
if stored is None:
return {"ok": True} # already handled: a retried delivery
state = await RunState.from_json(agent, stored)
for item in state.get_interruptions():
if body["approved"]:
state.approve(item)
else:
state.reject(item, rejection_message=body.get("reason") or "Rejected by the approver.")
result = await Runner.run(agent, state)
if result.interruptions:
... # another tool needs approval: save and hand off again
return {"ok": True}
What FlowRunner did: put the request in front of the approver, waited for them, kept a record of who answered and when on the flow’s Instances tab, and delivered the answer with retries. What your app still does, as OpenAI’s guide says it must: keep the state on the server, check the callback, claim each request atomically, and pass idempotency keys to tools with side effects.
The FlowRunner run can wait up to 30 days on Free, Starter, and Growth, and up to a year on Professional and above. However long it waits, it’s one execution.
Pattern: a flow as a function tool
When an agent needs data or an action from a business app, a flow can do it with FlowRunner’s integrations and return the answer in the same request. Build it with a prompt that says your program calls it through the API and waits, names the inputs, and spells out the answer you want back:
Using the FlowRunner tools, build a flow called “Order Lookup”. My program calls it through the API and waits for the answer. It gets
orderId. Look the order up in Shopify and returnstatus,total,items(name and quantity), andrefundable(true when it was delivered less than 30 days ago). If the order doesn’t exist, returnfound: false. When it’s built, make it live and tell me exactly what my program sends and gets back.
Then wrap the blocking Call Flow API in a tool:
@tool
def order_lookup(order_id: str) -> str:
"""Look up an order's status, total, items, and whether it can be refunded."""
r = requests.post(
f"{FR_BASE}/flows/<flow-id>/activate-blocking",
params={"waitResponseTimeoutSeconds": 30},
headers=FR_HEADERS,
json={"orderId": order_id},
timeout=40,
)
r.raise_for_status()
return r.text
The response body is whatever the flow’s Return Result block returns. The blocking call waits up to 300 seconds. If that runs out, the API answers with an error and the run keeps going, so a blind retry starts a second run. For anything that waits on a person, use the approval pattern above.
Pattern: FlowRunner calls your agent
The SDK has no inbound triggers, so something has to start the agent. A flow can:
Using the FlowRunner tools, build a flow called “Inbound Support Email”. When a new email arrives in the support@ Gmail inbox, POST its sender, subject, and body to
https://agents.example.com/ticketswith the headerX-Agent-Key; leave its value for me to paste into the HTTP Request block in the flow editor. Retry on 429 and 5XX up to 4 times with exponential backoff. If it still fails, forward the email to me with the error. Ask before making it live.
Gmail is one of FlowRunner’s 2,100+ integrations; a new ticket, a form submission, a CRM change, or a schedule (every weekday at 9 am, or a cron expression) works the same way. Your endpoint calls Runner.run with what it receives.
If your service exposes its tools over a public MCP server, you can also register that server in FlowRunner (Agent tools & Knowledge > MCP Servers) and attach its tools to a FlowRunner AI Agent, so flows other people build can call the same tools. See MCP servers in the docs.
Optional: turn agent code into a flow
FlowRunner Prompt to Flow can read Agents SDK code and build a flow that does the same work with FlowRunner’s AI Agent blocks, on your own model keys. We did this with six of OpenAI’s own Agent Builder templates, whose export is Agents SDK code, and wrote up what came across and what didn’t: six real conversions. The approval stubs in those exports became real approval steps.
That’s worth doing when the people who own the process need to read and change it on a canvas, or when you’re moving off Agent Builder anyway (how to move from Agent Builder). If your agent is part of your product’s code, keep it there and use the patterns above.
Questions about wiring OpenAI Agents SDK to FlowRunner? Contact us.
Frequently asked questions
Where do the FlowRunner workspace id and key come from?
Both are in Workspace Settings, under General, in the Credentials section. Keep the key on your server in an environment variable and send it in the FR-Access-Key header. Never put it in browser code.
Why not use the blocking call and wait for the approval?
The blocking call waits at most 300 seconds, and approvals take longer than that. Use the non-blocking call, which returns an executionId at once, and have the flow post the decision to your endpoint when it arrives.
What if the approver never answers?
The FlowRunner run waits up to 30 days on Free, Starter, and Growth, and up to a year on Professional and above. If you want a reminder or an automatic rejection after a set time, put it in the prompt that builds the flow, and your endpoint handles the rejection like any other.
Can one approval cover several tool calls?
Yes. The sample sends every pending interruption in one request and applies the approver's decision to all of them. If each call needs its own decision, send the list and have the flow return one decision per item.
Does this work with the TypeScript SDK?
Yes. Use needsApproval on the tool, store result.state.toString(), restore with RunState.fromString(agent, serialized), and call state.approve or state.reject. The FlowRunner calls are the same HTTP requests.
Ready to try it?
Start on the Free plan. No card required, and every new account gets 14 days of Professional.