Pre-interrupt() code re-runs on resume — anti-pattern, or is there a sanctioned way to detect resume?

Hello! I want to talk about point #3 from the interrupt docs (pre-interrupt code re-runs on resume)

In practice this is biting my team a bit:

  • We have logger.info(…) calls before interrupt() and now see duplicate log lines on every resume.
  • We have nodes whose pre-interrupt code is meant to run exactly once per interrupt so that a human can verify the work. To make that hold we’d have to split those nodes in two, pre-interrupt work in one node, and the interrupt + post-interrupt work in a successor. This which feels like ceremony forced by the runtime.

While digging we found a __pregel_resuming flag on get_config()[“configurable”] that’s essentially False on first invocation and True on resume. It lives in langgraph._internal._constants and also flips True on RetryPolicy replays, so it seems like a bad choice for product code.

Questions:

  1. Is there a public, supported way for a node to ask “am I being resumed?” or is one planned?
  2. Is the split-the-node approach the recommended pattern for once-per-run side effects, or is there a cleaner idiom we’re missing?

Setup: langgraph 1.1.2, Python 3.12.

Hello @Anthony ,

Not today. There’s no supported way for node code to ask “is this an interrupt resume?”

get_config()["configurable"]["__pregel_resuming"] is an internal flag ([CONFIG_KEY_RESUMING](https://github.com/keenborder786/langgraph/blob/076e2a3627206f5a1aef573aaca4a01e5af897ca/libs/langgraph/langgraph/_internal/_constants.py#L45) in langgraph._internal._constants). It’s used by the Pregel loop to decide how to apply input and propagate state to subgraphs. As you noticed, it’s also set on RetryPolicy replays (_retry.py), so it means “this tick is continuing from persisted state,” not strictly “human resumed an interrupt.” I wouldn’t rely on it in application code.

Re-running the node from the top on resume is intentional. The interrupt() docs say the graph resumes at the start of the node and re-executes all logic. interrupt() itself is the exception: on resume it returns the already-supplied value instead of raising again.

So for code in the same node as interrupt(), you should assume it runs again on every resume. That’s not an anti-pattern; it’s the contract.

For once-per-interrupt (or once-per-run) work:

A. Split nodes (StateGraph): Put setup / verification in a predecessor node, and keep interrupt() in a successor.

prepare → verify_once → ask_human (interrupt)

B. Persist idempotency in state: If you must stay in one node, write a marker to state (“verification payload already emitted”) and no-op on the second pass. Same idea as any replay-safe workflow.

C. Logging: duplicate: logger.info lines on resume are expected if logs sit before interrupt() in the same node. Practical fixes:

  • Move noisy logs to a predecessor node or after you know you’re past the interrupt boundary.
  • Log at the client when you handle __interrupt__ in the stream.
  • Keep node logs idempotent (same thread_id / interrupt.id in every line) and dedupe downstream if needed.

TL;DR: No supported resume-detection flag; split the node (or use state) for run-once work, that’s the intended model, not a workaround.

Thanks @keenborder786 ,

I didn’t think to flag in the state! I like that solution, but I think we’ll go with multiple nodes to start. I appreciate the quick and awesome response! Cheers!