Use LangGraph with FlowRunner
Send a LangGraph interrupt() to a person in Slack or email, wait days for the answer, resume with Command(resume=...), and pipe LangSmith Deployment webhooks into flows.
LangGraph already knows how to pause. A node calls interrupt(), the checkpointer saves the thread, and the graph waits until you resume it. This guide is about everything around that pause: getting the payload to the person who decides, waiting for them with reminders, bringing the answer back as Command(resume=...), and keeping a record of it. Then two smaller connections: finished runs on LangSmith Deployment flowing into business steps, and business events starting graph runs.
Your graph stays where it runs, in your process or on LangSmith Deployment. FlowRunner doesn’t run Python. You build the flows from a chat with FlowRunner Prompt to Flow, and the code below is plain requests and FastAPI around a compiled graph.
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.
The code needs the workspace id and API key from Workspace Settings > General > Credentials, in FLOWRUNNER_WORKSPACE_ID and FLOWRUNNER_KEY on the server. The key goes in the FR-Access-Key header.
Send an interrupt to a person, resume on the answer
The example: a graph handles refund requests. For anything over a set amount, a node pauses for the support lead. The support lead lives in Slack and has never heard of LangGraph.
The node. It pauses with everything the reviewer needs, and nothing else:
from langgraph.types import interrupt
def review_refund(state: RefundState):
decision = interrupt({
"customer": state["customer"],
"orderId": state["order_id"],
"amount": state["amount"],
"reason": state["reason"],
"agentSummary": state["summary"],
})
return {"approved": decision["approved"], "review_note": decision.get("note", "")}
Compile the graph with a durable checkpointer, graph = builder.compile(checkpointer=checkpointer). LangGraph’s docs call InMemorySaver a development tool, and this thread may wait through a few deploys.
Build the approval flow:
Build a flow called “Refund review” that starts from the Call Flow API. Its Initial Data is threadId, customer, orderId, amount, reason, agentSummary, and callbackUrl. Send the support lead a Slack direct message with those details and Approve and Reject buttons, then wait for the click. If there’s no answer in 4 hours, send a reminder. If there’s still none after 24 hours, post the request in #support-leads for anyone to answer. If nobody has answered after 3 days, treat it as rejected with the note “No answer in 3 days”. When there’s a decision, POST threadId, approved (true or false), the note, and this run’s Execution ID to callbackUrl, with an X-Callback-Secret header whose value I’ll paste into the block myself, and retry on 5XX and network errors. Before you build, tell me anything you can’t map one to one, and ask me about anything I haven’t said.
If the reviewer should change the amount rather than only approve or reject, a FlowRunner Form can show the request as editable fields behind a public link. FlowRunner Prompt to Flow can’t create Forms yet, so it tells you what to add in the flow editor.
Hand off the interrupt from the app. Send the request after invoke returns, not inside the node. On resume, LangGraph runs the node again from its start, so anything the node does before interrupt() happens twice:
import os
import requests
REVIEW_URL = (
"https://api.flowrunner.ai/api/v1/"
f"{os.environ['FLOWRUNNER_WORKSPACE_ID']}/flows/<flow-id>/activate"
)
def start_refund(request_id: str, refund: dict):
config = {"configurable": {"thread_id": request_id}}
result = graph.invoke(refund, config)
if "__interrupt__" in result:
payload = result["__interrupt__"][0].value
resp = requests.post(
REVIEW_URL,
headers={"FR-Access-Key": os.environ["FLOWRUNNER_KEY"]},
json={"threadId": request_id, **payload, "callbackUrl": os.environ["RESUME_URL"]},
timeout=30,
)
resp.raise_for_status()
save_pending(request_id, resp.json()["executionId"])
return result
The non-blocking Call Flow API returns the run’s executionId straight away, and the FlowRunner run waits for the support lead from there.
Resume on the answer. The flow POSTs the decision to RESUME_URL:
import hmac
import os
from fastapi import FastAPI, Header, HTTPException
from langgraph.types import Command
app = FastAPI()
@app.post("/refunds/resume")
def resume(body: dict, x_callback_secret: str = Header(default="")):
if not hmac.compare_digest(x_callback_secret, os.environ["CALLBACK_SECRET"]):
raise HTTPException(status_code=401)
if not mark_resumed(body["threadId"], body["executionId"]):
return {"ok": True} # already resumed: a retried delivery
config = {"configurable": {"thread_id": body["threadId"]}}
graph.invoke(
Command(resume={"approved": body["approved"], "note": body.get("note", "")}),
config,
)
return {"ok": True}
The value you pass in Command(resume=...) becomes the return value of interrupt(), so review_refund gets approved and note and the graph carries on. mark_resumed is your own check that this thread hasn’t already been resumed, since the flow’s Retry Policy can deliver twice if your endpoint fails partway.
What you get from the split: the thread waits in your checkpointer, the person is chased by the flow, and the FlowRunner run shows what was asked, the reminder, who it went to, and the answer, to anyone on the team. The flow can wait up to 30 days on Free, Starter, and Growth, and up to a year on Professional and above, and the whole review is one execution. This example handles one interrupt at a time; if your graph can raise several at once, send each one and match the answers to them before resuming.
Send finished runs into a flow
On LangSmith Deployment, you can pass a webhook URL when you create a run, and LangSmith sends a request when the run completes. The payload carries the run_id, the thread_id, the status, and, for stateful runs, the thread’s values. Point it at a flow and a finished run turns straight into business steps.
Build the flow:
Build a flow that starts with an External Callback and receives a LangSmith Deployment run webhook (run_id, thread_id, status, and values). If status is success and values.approved is true, create the refund note on the Zendesk ticket in values.ticketId, email the customer at values.customerEmail with the outcome, and post a summary to #support. If status is error or timeout, post the run_id and the error to #support-eng. Otherwise do nothing. Ask me about anything I haven’t said.
A run that stopped at an interrupt is reported with status success, which is why the flow checks the values and not only the status.
Keep the key out of the URL. Use the header form of the External Callback address, https://api.flowrunner.ai/api/v1/{workspace-id}/flows/{flow-id}/triggers/{block-id}/activate (the ids are in the block’s Callback URL, explained here), and set the key as a static webhook header in langgraph.json:
{
"webhooks": {
"headers": {
"FR-Access-Key": "${{ env.LG_WEBHOOK_FR_KEY }}"
}
}
}
By default LangSmith only reads environment variables with the LG_WEBHOOK_ prefix here, so name the variable that way.
Start a graph run from a business event
When a graph should run because something happened in a business app, a flow can catch it with that app’s trigger, with the account connected once, and call your graph with only the fields it needs.
Put a small endpoint in front of the graph that accepts the job and returns at once:
from fastapi import BackgroundTasks
@app.post("/refunds/start")
def start(body: dict, background: BackgroundTasks):
# check the Authorization header against your token here
background.add_task(start_refund, body["requestId"], body["refund"])
return {"accepted": True}
Then build the trigger side:
Build a flow that starts on a Zendesk ticket event when a ticket is tagged “refund-request”. Get the ticket and its comments. POST requestId (the ticket id) and refund (customer, orderId, amount, reason, and the latest comment) to https://agents.example.com/refunds/start, with an Authorization header I’ll fill in with my bearer token in the flow editor, retrying on 429, 5XX, and network errors. Add an internal note to the ticket saying the request is being reviewed. Ask me about anything I haven’t said.
The approval from the first section then happens inside that same graph run. If your graph runs on LangSmith Deployment, the flow can call the deployment over HTTP in the same way.
Let a FlowRunner agent call your graph
A FlowRunner AI Agent block can use tools from any MCP server you register in FlowRunner, as long as its URL is publicly reachable and it accepts a token header or OAuth 2.0. Agent Server exposes deployed agents as MCP tools at /mcp, and the LangChain guide shows a small MCP server written with the MCP Python SDK. Either one, registered in the console, lets a FlowRunner agent hand a task to your graph when it decides to. FlowRunner Prompt to Flow can’t register MCP servers yet, so that step is in the console.
Optional: turn a graph into a flow
Some graphs are a few model calls, some branches, and an approval. If you want one of those on the FlowRunner canvas, FlowRunner Prompt to Flow can read the graph code and build the equivalent flow:
Read this LangGraph graph and build the equivalent FlowRunner flow. Each node that calls a model becomes an AI Agent block with the same instructions, each conditional edge becomes a condition or router, and each interrupt() becomes a human approval step. Tell me which nodes run custom code that should stay in my service and be called over HTTP, and ask me about anything the code doesn’t say.
That’s an option, not a recommendation. A graph with custom state logic, your own models, or tuned retrieval belongs in LangGraph, and the patterns above connect it to FlowRunner where it runs.
Questions about wiring LangGraph to FlowRunner? Contact us.
Frequently asked questions
Why send the interrupt to FlowRunner from the app and not from inside the node?
Because LangGraph re-runs the node from its start when you resume. Code before interrupt() runs again, so a request sent from inside the node would go out a second time when the answer comes back. Sending it from the app, after invoke returns with an interrupt, sends it once.
Which checkpointer should I use when the answer can take days?
A durable one. LangGraph's docs call InMemorySaver a development tool and advise a durable checkpointer in production. A database-backed checkpointer keeps the paused thread through restarts and deploys, so the resume call works whenever FlowRunner delivers the decision. On LangSmith Deployment's Agent Server, checkpoints are handled for you.
What happens if nobody answers?
You decide in the flow. It can remind the reviewer, move the request to a backup approver, and after a deadline you set, send a decision of its own, such as a rejection with a note, so the graph doesn't sit waiting for an answer that isn't coming. A FlowRunner run can wait up to 30 days on Free, Starter, and Growth, and up to a year on Professional and above.
Can I use this with LangSmith Deployment instead of my own server?
Yes. The approval pattern works the same way, with your resume step calling the deployment to continue the thread. LangSmith Deployment's completion webhook can also point at a FlowRunner flow that starts with an External Callback, with the FlowRunner key set as a static header in langgraph.json.
Does the waiting approval flow count as many executions?
No. One complete flow run is one execution, however many steps it has and however long it waits. An approval that takes three days, with a reminder and an escalation, is one execution.
Ready to try it?
Start on the Free plan. No card required, and every new account gets 14 days of Professional.