LangGraph: idempotent tool calls after interrupt, resume and crashes

LangGraph restarts a node when it resumes after interrupt(), and a crash can repeat work done after the last checkpoint. How to stop repeats.

What LangGraph does on resume

LangGraph's own documentation on interrupts says that when a graph resumes, "the runtime restarts the entire node from the beginning", so code that ran before interrupt() runs again. Its advice: side effects placed before an interrupt should be idempotent, or placed after it. The same idea applies to crashes: work done after the last checkpoint is done again when the thread resumes.

Option 1: put side effects after interrupt()

def approve_and_pay(state):
    decision = interrupt({"pay": state["amount"]})   # resumes here with the human's answer
    if decision == "yes":
        pay(state["amount"], state["to"])            # side effect AFTER the interrupt

This avoids the re-run on resume after an interrupt. It does not help if the process crashes after pay and before LangGraph saves the node's result.

Option 2: make the tool itself once-only

Call4Agents has a LangGraph adapter: the thread id becomes the run id, so a resumed thread finds the actions it already did.

from call4agents.adapters import langgraph as c4a_lg
import call4agents as c4a

c4a.init()

@c4a_lg.effect                         # inside any node or tool
def send_email(to: str, body: str) -> str:
    return mailer.send(to=to, body=body)

# a human approval bound to the exact arguments they saw:
@c4a_lg.effect(approval=True)
def pay(amount: int, to: str) -> str:
    return payments.charge(amount=amount, to=to)

# when the interrupt is answered:
c4a_lg.approve(pay, config, amount=10, to="alice")
graph.invoke(Command(resume="yes"), config)

A repeat of the same action in the same thread returns the recorded result. If the arguments in the saved state were changed after the "yes" (for example to pay(9999, "mallory")), the call is refused.

pip install call4agents
call4agents quickstart   # offline, no API keys: 2 emails without the kit, 1 with it

Related guides