LangGraph interrupt before a paid API call
Put LangGraph's interrupt() in its own node ahead of the node that calls a paid video API, because a resume re-runs the whole node from its top.

To pause a LangGraph run before it spends money, call interrupt() in a node of its own, compile the graph with a checkpointer, and run with a thread_id. Put the paid POST /v1/videos in the next node: LangGraph restarts the whole node that called interrupt() on resume, so a paid call placed before the interrupt in the same node would run again.
The LangGraph facts come from its Interrupts page, read 2026-09-29. The Sume facts come from Video Generation and Jobs and results. Sume has no LangGraph connector; the node below is a plain HTTPS call.
Why does the interrupt need its own node?
The LangGraph docs say that on resume the runtime restarts the entire node from the beginning, not from the line where interrupt() was called, so code that ran before the interrupt runs again. They list three safe patterns for side effects: use idempotent operations before the interrupt, place side effects after it, or separate them into different nodes. Their own example of the failure is an API call that updates a record and then re-runs on every resume.
For a paid video job that maps to two nodes: approve holds only the interrupt and the routing, and submit holds the request. Nothing that spends money can run twice from a resume.
Which side-effect placements are safe?
The docs' checklist for side effects around an interrupt, applied to a paid submit:
| Placement | Docs verdict | For a paid video submit |
|---|---|---|
| Idempotent operation before the interrupt | Recommended | Only if the request carries a stable Idempotency-Key |
| Side effect after the interrupt call | Recommended | Works, but the node still re-runs from the top on any later resume |
| Side effect in a separate node | Recommended when possible | The pattern used below |
| Non-idempotent call before the interrupt | Can overwrite or duplicate on each resume | A second paid job |
What does the graph look like?
This is a runnable sketch. The Idempotency-Key is built from the thread_id, so use one thread per video request. If the node itself is retried, a replay of the same key and body returns the original job instead of a second charge.
import os, requests
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
class S(TypedDict):
prompt: str
job_id: str
def approve(state: S):
ok = interrupt({"question": "Start this paid video?", "prompt": state["prompt"]})
return Command(goto="submit" if ok else END)
def submit(state: S, config):
r = requests.post(
"https://api.sume.com/v1/videos", timeout=30,
json={"model": "sume/auto", "prompt": state["prompt"]},
headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
"Idempotency-Key": "lg-" + config["configurable"]["thread_id"]})
r.raise_for_status()
return {"job_id": r.json()["id"]}
g = StateGraph(S)
g.add_node("approve", approve); g.add_node("submit", submit)
g.add_edge(START, "approve"); g.add_edge("submit", END)
app = g.compile(checkpointer=InMemorySaver())
cfg = {"configurable": {"thread_id": "video-42"}}
print(app.invoke({"prompt": "A desk lamp turning on"}, cfg)["__interrupt__"])
print(app.invoke(Command(resume=True), cfg))What do I need for the pause to work?
The docs say the graph waits indefinitely until you resume, so an unanswered approval costs nothing.
- A checkpointer. The docs use
InMemorySaverin examples and say to use a durable checkpointer in production. - A
thread_idinconfig. The docs call it your persistent cursor: reusing it resumes the same checkpoint, and a new value starts a new thread with empty state. - A JSON-serializable payload in
interrupt(). The docs show a question plus details, which is what your UI renders. - With the default
invoke(), the pending payload appears underresult["__interrupt__"]; with event streaming it appears onstream.interrupts. - Resume with
Command(resume=True)to approve orCommand(resume=False)to reject, as the docs' approve-or-reject pattern does.
What should I not do around interrupt()?
The docs warn against wrapping interrupt() in try/except: the pause works by raising a special exception, so a broad handler swallows it and the interrupt never reaches the graph. Keep the request, with its raise_for_status() and any error handling, in the submit node. Note also that an approval here is a graph decision, not a Sume feature; Sume's Idempotency-Key only makes a retried submit safe.
How do I follow the job after it starts?
The submit returns a job id right away with a 202; the clip takes longer. Add a polling node, or use a webhook, as Jobs and results describes. The docs say a video's price is reserved on submit at provider list × 1.25, so a rejected approval never reaches the reservation. For the polling loop itself, see poll an AI video job.
Sources
Related posts
More in Integrations
- Looping by Zapier: send one API request per item in a list
Looping by Zapier runs every later action once per list value, all in parallel, up to 500 times. Pace and key paid API calls to match.
- Make.com error handling: retry a paid API call without duplicates
Make.com has five error handlers; Retry stores the failed bundle and reruns it. Send a fixed Idempotency-Key so a rerun can't bill twice.
- MCP config API key: use an environment variable, not the file
Cursor, Claude Code, Windsurf and Codex can read an MCP API key from an environment variable. Here is each client's syntax and what an unset variable does.
- MCP OAuth rejects a callback with no iss: what to do
Gemini CLI and Codex validate the RFC 9207 iss parameter on the OAuth callback. Which clients reject a missing iss, and what a server without iss can rely on.
Written by Sume