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.

5 min readSume
All posts

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:

From the LangGraph Interrupts page, read 2026-09-29.
PlacementDocs verdictFor a paid video submit
Idempotent operation before the interruptRecommendedOnly if the request carries a stable Idempotency-Key
Side effect after the interrupt callRecommendedWorks, but the node still re-runs from the top on any later resume
Side effect in a separate nodeRecommended when possibleThe pattern used below
Non-idempotent call before the interruptCan overwrite or duplicate on each resumeA 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 InMemorySaver in examples and say to use a durable checkpointer in production.
  • A thread_id in config. 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 under result["__interrupt__"]; with event streaming it appears on stream.interrupts.
  • Resume with Command(resume=True) to approve or Command(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

All Integrations posts

Written by Sume