Static breakpoints are fixed at compile time, while dynamic breakpoints allow nodes to trigger interruptsbased on conditions during runtime.
Learn to use langgraph.types.interrupt to achieve a truly flexible human-computer collaboration process.
In the first few lessons of Module 3, we learnedStatic Breakpoints: When compiling the graph, passcompile(interrupt_before=["node_name"])orinterrupt_after=to specify which nodes will trigger a pause.
Static breakpoints have a fundamental limitation:It is determined at "compile time" and has nothing to do with runtime data.。
| Dimensions | Static Breakpoint |
|---|---|
| Set location | When graph is compiled:builder.compile(interrupt_before=[...]) |
| Trigger condition | Each time it is executed to this node,unconditionallyinterrupt |
| Flexibility | Unable to decide "whether to interrupt" based on input data |
| Interruption reason | Fixed node name, no additional information |
| Typical scenario | Forced observation of the output of a node during debugging and development |
Imagine an approval workflow: when AI-generated contentexceed a certain risk thresholdOnly when manual review is required, low-risk content should be passed directly. When implemented with static breakpoints, you can only "either always break or never break" - it cannot be doneconditional interrupt。
this isDynamic BreakpointsReason for existence: Let the code inside the node function, based on the runtime data,self-determinationWhether an interrupt is triggered and the specific reason for the interrupt can be communicated to the outside world.
(1) Approval: Interruption graph, which presents the status to the user and allows the user to decide whether to continue.
(2) Debugging: Roll back the graph to a certain checkpoint to reproduce or avoid the problem
(3) Editing: Modify the intermediate state and change the subsequent behavior of the graph
Dynamic breakpoints mainly serve the (1) scenario, but are more accurate than static breakpoints——Only bother humans when needed。
LangGraph bylanggraph.types.interruptFunction implements dynamic breakpoints. When a node function is called during executioninterrupt(...)When LangGraph runs:
interrupt()A LangGraph-aware controlled interrupt is triggered internally, and the runtime takes over without letting the program crash with an uncaught exception.
The complete state of the graph (including the output of executed nodes) is persisted toMemorySaveror other checkpointers.
passed tointerrupt()The payload (it is recommended to use JSON serializable strings, dictionaries, etc.) will be exposed to the caller and recorded instate.tasksofinterruptsin the field.
graph.stream()orgraph.invoke()When the call returns, the process stops at the node that triggered the interruption; it can be continued through manual input, status modification, or recovery commands.
from langgraph.types import interrupt
# 调用 interrupt() 会暂停当前图执行
# payload 会作为中断原因暴露给外部调用方
interrupt("需要人工审核:输入内容超出长度限制")
interrupt({"reason": "high_risk", "score": 0.95})
Unlike static breakpoints set at the "graph structure level",interrupt()is inBusiness logic level of node functionTriggered. This means you can use any Python logic to decide whether to break: if conditions, model confidence thresholds, rules engines, external API calls... total freedom.
The example in the notebook builds a simple three-step diagram:step_1 → step_2 → step_3, among whichstep_2Will be called when the input string exceeds 5 charactersinterrupt()。
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, END, StateGraph
from langgraph.types import interrupt # ← 导入动态中断函数
# State 只有一个字段:input 字符串
class State(TypedDict):
input: str
# ── step_1:普通节点,直接返回 state ──
def step_1(state: State) -> State:
print("---Step 1---")
return state
# ── step_2:包含动态断点逻辑的关键节点 ──
def step_2(state: State) -> State:
# 运行时条件判断:如果输入超过 5 个字符,则中断
if len(state['input']) > 5:
# 调用 interrupt(),并附带说明原因的 payload
interrupt(
f"Received input that is longer than 5 characters: {state['input']}"
)
# 如果没有触发中断,则正常执行
print("---Step 2---")
return state
# ── step_3:普通节点 ──
def step_3(state: State) -> State:
print("---Step 3---")
return state
ifExecute when conditions are metinterrupt(...), the execution of this graph will be paused here,NoContinue executionprint("---Step 2---"). Only when the conditions are not met, will you go downprintandreturn state. This is the essence of "conditional interrupt".
# 构建图结构
builder = StateGraph(State)
builder.add_node("step_1", step_1)
builder.add_node("step_2", step_2)
builder.add_node("step_3", step_3)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", END)
# 准备 MemorySaver(动态断点依赖 checkpointer 保存状态)
memory = MemorySaver()
# 编译时 ★ 不需要 ★ 指定任何 interrupt_before / interrupt_after!
# 动态断点完全由节点内部的 interrupt() 调用控制
graph = builder.compile(checkpointer=memory)
Static breakpoint:builder.compile(checkpointer=memory, interrupt_before=["step_2"])
Dynamic breakpoints:builder.compile(checkpointer=memory) ← No additional parameters required
All the logic of dynamic breakpoints is in the node function, and the compiler does not need to know any information.
There is no trace of "interruption" in the graph structure itself - dynamic breakpoints "appear" only at runtime. This is different from the visibility of static breakpoints (break nodes are fixed at compile time).
# 配置:使用 thread_id 追踪这次执行的状态
initial_input = {"input": "hello world"} # 11 个字符,超过 5
thread_config = {"configurable": {"thread_id": "1"}}
# 使用 stream 运行图
for event in graph.stream(initial_input, thread_config, stream_mode="values"):
print(event)
Observe the output:
{'input': 'hello world'}: The initial State event when the graph starts---Step 1---: step_1 executes normally and prints{'input': 'hello world'}: State event after step_1 is executed__interrupt__:step_2 calledinterrupt(), the payload is returned to the callerstream()Returninterrupt()Will not cause the program to throw uncaught exceptions or crash.stream()Return normally, but end the iteration early. All interruption information is saved in the checkpointer, waiting for query or recovery.
state = graph.get_state(thread_config)
print(state.next)
state.nextshow('step_2',), indicating that the graph stopped at the "about to be executed" position of step_2 - step_2 triggered an interrupt, but this execution was aborted, so it is still the "next node to be executed".
print(state.tasks)
Interpretationstate.tasksKey fields of:
| Field | value | meaning |
|---|---|---|
name |
'step_2' |
The name of the node that triggered the interrupt |
interrupts[0].value |
'Received input that is longer than 5 characters: hello world' |
passed tointerrupt()The payload——Interruption reason, readable by humans or code |
interrupts[0].when |
'during' |
Interrupts occur "during" the execution of a node (as distinct from'before'/'after', that is, the location of the static breakpoint) |
error |
None |
This is not an error, it is a controlled interrupt |
pass tointerrupt()The value is not limited to strings, it is recommended to use JSON serializable structured data. For example:
interrupt({"reason": "high_risk", "score": 0.97, "flagged_content": text})
This allows human-machine interfaces to display detailed information and help humans make more accurate judgments.
Mistakes that beginners often make: Thinking that they will not modify the status or provideCommand(resume=...), directlygraph.stream(None, thread_config)can continue. Let's see what happens:
# 尝试直接恢复(不修改 state)
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
state = graph.get_state(thread_config)
print(state.next) # 仍然是 step_2!
becausestate['input']still"hello world"(11 characters), step_2 execution conditionlen(state['input']) > 5is still true, sointerrupt()meetingcalled again. If you keep usingNoneResuming without changing the conditions, the graph will repeatedly stop at the same interruption point.
This differs from the behavior of static breakpoints:
interrupt_before=["step_2"]It is interrupted before the execution of step_2, and step_2 starts to execute after recovery. Whether it passes or not depends on the code logic of step_2.NoneRecovery will run step_2 again. If the status has not changed and the interrupt condition is still true, it will be interrupted again.The break conditions of dynamic breakpoints are data-driven. This example uses "modify state first, then useNone"Recovery" method, so that the node no longer meets the interruption conditions when it is re-run. Another type of human-computer interaction process usesCommand(resume=...)Pass manual input backinterrupt()call point.
The correct way to handle it is: use it firstgraph.update_state()Modify the state so that the interrupt condition is no longer met, and then resume execution.
# 将 input 改为短字符串(5 个字符以内),让 step_2 的条件不再触发
graph.update_state(
thread_config,
{"input": "hi"}, # "hi" 只有 2 个字符,len("hi") = 2 ≤ 5
)
update_stateReturns the new checkpoint configuration, indicating that the status was successfully written.
# 传入 None 作为 input,表示"从上次中断处继续,不添加新输入"
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
This execution was successful! Execution process analysis:
{'input': 'hi'}:Current State at the time of recovery (has been modified)---Step 2---: step_2 is executed normally (len("hi") = 2 ≤ 5, does not trigger an interrupt){'input': 'hi'}: State event after step_2 execution---Step 3---: step_3 executes normallygraph.stream(initial_input, config)state.tasks[0].interrupts[0].valuegraph.update_state(config, new_values), so that the interrupt condition no longer holdsgraph.stream(None, config), the pending node re-runsIf you want to use manual input asinterrupt()The return value is passed back to the node and should be usedCommand(resume=...). This example demonstrates the status editing process: first useupdate_state()Eliminate the interrupt condition and usegraph.stream(None, config)Let the pending node run again.
In a production environment, graphs are typically deployed on the LangGraph server (vialanggraph devStart a local development server) and clients interact with it through the LangGraph SDK. Dynamic breakpoints are also applicable in API scenarios, and the operation logic is completely consistent with the local one.
in module/studioRun in the directory:
langgraph dev
After the service starts, it will display:
from langgraph_sdk import get_client
# 连接到本地开发服务器
URL = "http://127.0.0.1:2024"
client = get_client(url=URL)
# 创建新线程(相当于一次独立的图执行会话)
thread = await client.threads.create()
input_dict = {"input": "hello world"} # 超过 5 字符,会触发 interrupt()
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="dynamic_breakpoints", # 图的名称(在 langgraph.json 中定义)
input=input_dict,
stream_mode="values",
):
print(f"Receiving new event of type: {chunk.event}...")
print(chunk.data)
End of flow event - Figure in step_2interrupt()paused.
current_state = await client.threads.get_state(thread['thread_id'])
print(current_state['next'])
# 更新 state:将 input 改为短字符串
await client.threads.update_state(thread['thread_id'], {"input": "hi!"})
# 恢复执行:传入 input=None 表示从中断处继续
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="dynamic_breakpoints",
input=None, # ← 关键:None 表示恢复,而非新的输入
stream_mode="values",
):
print(f"Receiving new event of type: {chunk.event}...")
print(chunk.data)
The graph was successfully restored and executed (both step_2 and step_3 were executed normally).
In a production system, the graph runs on the server, and the "modify state" and "resume execution" operations come from another service (such as a human review interface, message queue, webhook callback). LangGraph SDK provides cross-process and cross-service status operation capabilities, making distributed human-computer collaboration workflow possible.
Both breakpoint mechanisms have their own applicable scenarios, and understanding their differences can help make the right choice in actual projects.
| Contrast Dimensions | Static Breakpoint | Dynamic Breakpoint |
|---|---|---|
| Set location | When the graph is compiled,compile(interrupt_before/after=[...]) |
Inside the node function,interrupt(...) |
| import | No additional import required | from langgraph.types import interrupt |
| Interruption timing | Node executionbefore(before) orafter(after) |
Node executionperiod(when='during') |
| Trigger condition | unconditionally: Interrupt every time it reaches the specified node. | Conditional: It is judged by the developer using if in the node function. |
| Interruption reason transfer | Only know "at which node it was interrupted" | A JSON serializable string, dictionary, etc. payload can be passed as the reason |
| direct resume effect | You can resume directly and the node will execute normally. | use NoneWhen restored and conditions remain unchanged, it willinterrupted again; useCommand(resume=...)Can pass human input back to the node |
| Pre-reinstatement requirements | You can choose to modify or not modify the state before resuming | In this example, the state release condition is first modified; it is used when manual input needs to be returned.Command(resume=...) |
| Read interrupt information | state.next(next node name) |
state.tasks[0].interrupts[0].value(specific reasons) |
| Suitable for the scene | During debugging and development, it is necessary to observe the output of a certain node regularly. | Manual review and risk interception triggered by conditions in the production environment |
| Typical code | graph = builder.compile(checkpointer=memory, interrupt_before=["step_2"]) |
within nodeif condition: interrupt("reason") |
Static breakpoints and dynamic breakpoints are not mutually exclusive. You can use both in the same picturecompile(interrupt_before=[...])Set static breakpoints and use them in some node functionsinterrupt(...)Set dynamic breakpoints. They work independently without interfering with each other.
| business scenario | Which breakpoint to use | break condition |
|---|---|---|
| AI content review system | dynamic breakpoint | Interrupt when content risk score > 0.8 for manual review |
| Financial approval workflow | dynamic breakpoint | When the reimbursement amount is > 1,000 yuan, it will be interrupted and wait for approval from the supervisor. |
| Code generation agent debugging | static breakpoint | During development, every time code is generated, it is paused to allow developers to check it. |
| Legal document generation | dynamic breakpoint | Interrupt when specific sensitive terms are involved and continue after confirmation by legal staff |
| Tool calls Agent security protection | dynamic breakpoint | Breaks when LLM decides to invoke "dangerous tools" (deletion, outgoing data) |
| Research assistant prototype verification | static breakpoint | Pause after each search node is completed to verify the quality of the search results |
In a production system, it is recommended tointerrupt()pass onstructured dictionaryInstead of a simple string, the review interface can better parse and display the information:
interrupt({
"reason": "high_risk_content",
"risk_score": 0.94,
"flagged_text": state["generated_content"][:200],
"suggested_action": "请人工审核后选择批准或拒绝"
})