Unit 06.02: Resuming without repeating side effects
Persistence makes resuming possible. It does not make resuming safe - that still requires the guard from Module 5, now reading a value that survived a restart.
The flag has to be in the checkpoint
The guard pattern and the checkpointer combine: the flag is written into state, the checkpointer persists it, and the node reads it on re-entry.
The code invokes twice on one thread and counts the emails.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
EMAILS = []
class State(TypedDict):
notified: bool
step: str
def notify(state: State):
if state["notified"]:
return {"step": "notify skipped -- already done"}
EMAILS.append("customer notification")
return {"notified": True, "step": "notify sent"}
g = StateGraph(State)
g.add_node("notify", notify)
g.add_edge(START, "notify")
g.add_edge("notify", END)
app = g.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "run-1"}}
print(app.invoke({"notified": False, "step": ""}, config))
print(app.invoke(None, config))
print(f"\nemails sent: {len(EMAILS)}")
# The checkpoint restores `notified`, and the guard reads it. Persistence alone
# does not make resuming safe -- it makes the flag available, and the node has
# to check it.
One email across two invocations. The checkpoint restored notified, and notify read it and returned early.
That is the whole mechanism, and it only works because the flag lives in state. A guard using a module-level variable passes this exact test in a single process and fails as soon as the resume happens in a different one - which is what a resume usually is.
The mistake this prevents
The mistake is assuming the checkpointer prevents re-execution. It restores state; it does not know which of your nodes have side effects. LangGraph will happily re-enter a node on resume, and the node has to decide for itself whether the effect already happened.
Takeaway
Persistence gives the guard flag somewhere durable to live. The node still has to check it - and the flag must be in state, not in a variable that does not survive the process.
