Two ways to pause, and when each one fits
Intermediate Lesson 15's interrupt() always paused, unconditionally, wherever it was called. That's the right fit when every single run needs a human to look at it. But often, only some runs deserve a pause, a $5 purchase doesn't need approval, a $50,000 one does. This lesson covers both mechanisms side by side: a static breakpoint, declared once at compile time, that pauses every run without exception, and a dynamic breakpoint, a node that only calls interrupt() when a runtime condition is actually met.
The dynamic breakpoint: a node that sometimes pauses
def maybe_require_approval(state: State) -> dict: if state["amount"] > APPROVAL_THRESHOLD: decision = interrupt( f"Payment of ${state['amount']:.2f} exceeds ${APPROVAL_THRESHOLD}. Approve? (yes/no)" ) return {"approved": str(decision).strip().lower() in {"yes", "y"}} return {"approved": True}This node runs on every invocation, but interrupt() is only reached when the condition is true. A $300 payment sails straight through with no pause at all, a $900 payment stops here and waits. Same compiled graph, same node, different behavior, entirely driven by the data.
The static breakpoint: pauses no matter what
static_app = static_builder.compile( checkpointer=InMemorySaver(), interrupt_before=["process_payment"],)interrupt_before=["process_payment"] is a compile-time declaration: before this node ever runs, for any input, pause. There's no interrupt() call inside process_payment at all, and no condition to check, the pause is unconditional and lives entirely outside the node's own code.
Resuming each kind
# Dynamic: interrupt() is waiting for a specific value.result = dynamic_app.invoke(Command(resume=approve_with), config)
# Static: nothing is waiting for a value, just continue past the pause.result = static_app.invoke(None, config)Both use the same underlying checkpointer-backed pause/resume mechanism from Lesson 15. The difference is only in what resuming means: a dynamic breakpoint's interrupt() call is waiting for a specific answer (Command(resume=...) supplies it), while a static breakpoint has no interrupt() call to satisfy, invoking with None simply lets the graph proceed past wherever it paused.
Checkpoint
- static breakpoint:
compile(interrupt_before=[...]), pauses before a named node, every single run, unconditionally. - dynamic breakpoint: a node that calls
interrupt()only when a runtime condition is met, same node runs every time, the pause doesn't. Command(resume=...): supplies the value aninterrupt()call is waiting for.- resuming a static breakpoint:
invoke(None, config), there's no value to supply, just permission to continue.
If anything here still feels unclear, ask before moving to Lesson 31.