Approve, reject, or edit
The last lesson paused and wrote down the answer. In real use the answer should change what happens next, and three shapes cover almost everything.
One: approve or reject
The answer picks the route. Return a Command, from lesson 11, and one node both asks and decides.
from typing_extensions import TypedDict
class State(TypedDict):
amount: int
outcome: strfrom typing import Literal
from langgraph.types import interrupt, Command
def approve(state) -> Command[Literal["refund", "decline"]]:
said = interrupt({"question": "Refund?", "amount": state["amount"]})
return Command(goto="refund" if said == "yes" else "decline")Passing a dictionary rather than a string means the person deciding sees the amount as well as the question, which is what they actually need. The answer then chooses the destination, so approving and declining are two ordinary nodes.
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START
builder = StateGraph(State)
builder.add_node("approve", approve)
builder.add_node("refund", lambda s: {"outcome": f"refunded {s['amount']}"})
builder.add_node("decline", lambda s: {"outcome": "declined"})
builder.add_edge(START, "approve")
graph = builder.compile(checkpointer=InMemorySaver())for thread, said in [("a", "yes"), ("b", "no")]:
config = {"configurable": {"thread_id": thread}}
graph.invoke({"amount": 500, "outcome": ""}, config)
print(said, "->", graph.invoke(Command(resume=said), config)["outcome"])Two threads, two answers, two different endings, from one graph. Each thread paused on its own and was answered on its own.
Two: edit what it wrote
Instead of yes or no, hand back a corrected version and the node uses what it is given.
from typing_extensions import TypedDict
from langgraph.types import interrupt
class State(TypedDict):
reply: str
def check_reply(state):
edited = interrupt({"draft": state["reply"]})
return {"reply": edited}The draft goes out so a person can see what the machine wrote, and whatever comes back replaces it. If they changed nothing, nothing changes.
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START
builder = StateGraph(State)
builder.add_node("check_reply", check_reply)
builder.add_edge(START, "check_reply")
graph = builder.compile(checkpointer=InMemorySaver())from langgraph.types import Command
config = {"configurable": {"thread_id": "edit-1"}}
paused = graph.invoke({"reply": "we will refund you eventually"}, config)
print("shown to a human:", paused["__interrupt__"][0].value)
print("after editing: ", graph.invoke(Command(resume="Your refund is on its way."), config)["reply"])Three: ask inside the tool
The neatest version. The tool that does the dangerous thing asks for permission itself, so no graph anywhere has to remember to check.
from langchain_core.tools import tool
from langgraph.types import interrupt
@tool
def refund(amount: int) -> str:
"""Refund an amount in rupees to the customer."""
if interrupt(f"Approve a refund of {amount} rupees?") != "yes":
return "The refund was not approved."
return f"Refunded {amount} rupees."
print(refund.name, "->", refund.description)That is only the tool, so there is nothing to pause yet. Put it in an agent and every refund it ever attempts stops for a human, without one extra line in the graph. Lesson 31 does exactly that.
| Shape | What comes back | Good for |
|---|---|---|
| Approve or reject | "yes" or "no" | Money, deletions, anything one way |
| Edit | A corrected value | Replies, drafts, anything with wording |
| Ask inside the tool | Either of the above | When the rule belongs to the action, not the graph |
- Add a third route for
"ask my manager". - In the editing example, resume with the draft unchanged and confirm nothing breaks.
- Give the refund tool a limit, so only refunds over 1000 stop to ask.
This is what real progress feels like.