Designing the approve / edit / reject surface
Three human verdicts map to three different resume payloads and three different downstream paths — and the edit case is the one that decides whether your state schema was designed for correction or only for display.
A broker opens the review screen. One field reads invoiceTotal: 1042.50 at 0.62 confidence, and the crop beside it clearly shows 1075.00. What are they allowed to do about it?
In the version of this screen you have already shipped, the answer is approve or correct, and correcting writes a value into your database before the downstream step fires. That works because the agent already finished. Once the same review is a graph-level pause, the correction is not a database write. It is an argument to a resume call, which becomes a local variable inside a node that is about to act on it. Designing that argument is designing the product.
The one line the mechanism hangs on
The interrupts page shows a tool review, and it is the example to work from because it is the only worked review payload the docs give:
interrupt({ action: "send_email", to, subject, body,
message: "Approve sending this email?" });Alongside it, the page notes that the resume value can override inputs before executing. That sentence is small and it is the whole lesson. The value coming back is not restricted to a yes or no about the inputs the node already had. It can carry replacements for them, and the node can use those instead.
Which means correction is not a separate feature you bolt on beside approval. It is the same return value with a richer shape. Everything below follows from taking that seriously.
Three verdicts, three payloads, three routes
Approve, edit and reject is this course’s own vocabulary. LangChain documents the mechanism and does not name this taxonomy anywhere; a search of the interrupts page finds the override sentence and no pattern called approve/edit/reject. Use the three names because they map cleanly onto what a reviewer does, not because a framework endorses them.
Each verdict is a different resume value and a different way out:
- Approve — the node proceeds with the inputs it already had. The resume value can be as small as a discriminator. Its job is to say “a person saw this,” which is also the moment you want an identity and a timestamp for the audit trail.
- Edit — the resume value carries corrected data, and the node uses that in place of its original inputs. This is the verdict that touches state design, because the corrected value has to go somewhere that survives.
- Reject — the node does not do the thing. That is not the same as cancelling the run. Rejection is a downstream path with its own screen: a document routed to manual handling, a task reassigned, a reason captured. If your design treats reject as “close the modal,” you have a branch with no interface.
Written as a type on your side of the wire, which is where the type has to live:
type Verdict =
| { verdict: 'approve' }
| { verdict: 'edit'; value: number }
| { verdict: 'reject'; reason: string }And the node’s side, as this course’s own worked example rather than a sample copied from a doc page:
const decision = interrupt({
action: "extractField",
field: "invoiceTotal",
proposedValue: 1042.5,
message: "Approve, edit, or reject this extracted value?"
});
// decision, once resumed, is whatever Command({ resume }) supplied —
// e.g. { verdict: "edit", value: 1075.0 }Nothing here is typed for you
The signature is interrupt<I = unknown, R = any>(value: I): R. The payload defaults to unknown and the return defaults to any. LangGraph does not know what a verdict is, will not reject a malformed one, and will hand your node whatever JSON arrived.
That has two practical consequences and you already know how to handle both, because this is ordinary API-boundary work.
First, the union above is a contract you are asserting, so validate it where the resume value enters the node rather than trusting the annotation. A resume payload crosses a network boundary and comes from a client; it deserves the same treatment as a request body. Second, an any flowing out of interrupt() will silently infect everything downstream of it in a strict codebase, so give it an explicit type at the call site and narrow from there.
The edit case is a question about your state schema
Here is where the three-verdict design stops being a UI exercise. When a broker corrects 1042.50 to 1075.00, what does the graph hold afterwards?
If the channel holding extracted fields overwrites — the default behaviour when no reducer is declared — then the corrected value replaces the extracted one and the original is gone from state. Your checkpoint history still contains earlier snapshots, so the value is recoverable by walking backwards, but nothing in current state says a human changed anything. A screen that wants to show “extracted 1042.50, corrected to 1075.00 by Dana” has no channel to read.
If instead the schema keeps the model’s proposal and the human verdict as separate channels, or accumulates corrections into an append-only log, that screen is a read. Same graph, same interrupt, same three verdicts. Different schema, and the difference decides whether a before-and-after view is a query or a project.
This is the finding to bring to whoever owns the graph, and it has to arrive before the schema is settled rather than in design review. The state-as-contract lesson earlier in the course is the reducer detail; the point here is that a verdict design implies a schema requirement, and the person who notices is usually the one designing the surface.
Where people get burned
Do not make interrupts conditional on the verdict. The interrupts page warns against reordering or conditionally skipping interrupt calls, because pending interrupts are matched to resume values by index — so a node whose second interrupt only fires for an edit verdict can pair a resume value with the wrong pause. If a correction needs a second confirmation, put it in its own node with its own unconditional interrupt.
Reading a verdict design off somebody else’s graph
Reverse the exercise, since that is the actual job. You open an unfamiliar node and find an interrupt() call. What can you say about the product before anyone shows you a screen?
- The payload argument lists what a reviewer is entitled to see. A thin payload is either a thin screen or a screen doing extra fetches, and it is worth finding out which.
- Whatever the code does with the return value enumerates the verdicts. A single
if (decision)means two verdicts exist, whatever the roadmap says. A switch over a discriminator gives you the branch list directly. - Any branch that reassigns an input from the resume value is a correction path, and correction paths need an editable control rather than a button.
- Any branch that routes elsewhere instead of proceeding is a rejection path, and it owes the user a destination.
That is four surface findings from one function body, none of which required running anything.
Retrieval check
A node interrupts with a payload, and the only thing it does with the return value is `if (approved) { …proceed… }`. What have you learned, and what should you ask?
Check your answer
You have learned the product currently supports two verdicts, and the negative one has no path. The else is implicit, so rejection either does nothing or falls through to whatever follows the block. That is a branch users will reach with no interface behind it.
Ask two questions. What is supposed to happen to a rejected item — manual queue, reassignment, discard with a reason — because that answer is a screen nobody has scheduled. And can a reviewer correct rather than only judge, because if the answer is yes, the resume value has to carry data instead of a boolean, and the state schema has to have somewhere to keep it. Both questions are cheap now and expensive after the schema is settled.
Check your recall
Answer from memory — no scrolling back.
Hands on
Design all three verdicts, and find the schema requirement
Done when: ARTIFACT.md’s module 2 section holds a discriminated union for the resume value with all three verdicts, a named destination for the reject path, and one sentence stating what the state schema must hold for a before-and-after view to be a read rather than a history walk.
- Start from the payload type you wrote in the interrupt-and-resume hands-on. Do not change it yet.
- Write the resume value as a discriminated union with all three verdicts. Give the edit case a real field type — a corrected value, not
unknown— and give the reject case whatever the downstream path needs, which is usually a reason and sometimes a destination. - Write the node’s branch for each verdict in one line each. Three verdicts, three routes out. If two of them do the same thing, say so explicitly and defend it — that is a real design choice, not an oversight, but it should be a choice.
- Name the reject destination. Not “show an error” — a screen or a queue a real broker ends up in. Add it as a row in module one’s node table if it needs a node.
- Now go back to the state table from the state-as-contract lesson. Decide where a corrected value lands and whether the model’s original proposal survives in current state. Write the one sentence that says what a before-and-after view costs under your current schema.
- Add the validation. One line naming where the resume payload gets checked against the union, on the grounds that it arrives from a client over a network.
- Bring the union and the schema sentence into the chat. I will push on a reject path with no destination, and on an edit case whose corrected value has nowhere to live.
What this does not cover
This lesson assumed the node can be paused and resumed safely, which is true for a node that only proposes a value. It stops being true the moment the reviewed thing is an action with a side effect, because a resumed node restarts from its first line rather than from the interrupt() call. Where the side effect sits relative to that call decides whether a resume is free or expensive, and it also decides whether the value a human approved is the value that actually runs. The mid-tool-call lesson in this module is that rule.
It also left the reviewer’s identity alone. Who approved, when, and whether that is a channel in graph state or a record in your own application is a real design question, and it turns into a data question the moment the checkpoint store is somewhere different from your database. The showable-state lesson in the state-and-time module is where that boundary gets drawn.
Nothing here addressed a reviewer who wants to see the correction they made an hour ago and change their mind. That is replay and forking, which is a different primitive with its own lesson later in the course.
Read this next — primary source
InterruptsLangChain — docs.langchain.com, JavaScript docs, fetched 2026-09-05. Vendor documenting its own product
The same page the interrupt-and-resume lesson opened, read for a different sentence. Its tool-review example is the one worked case the docs give for a review payload, and one line in that section — that the resume value can override inputs before executing — is the entire mechanism behind a correction verdict. Read the tool-review section closely and notice how little the page prescribes: it shows a mechanism and leaves the vocabulary of verdicts, the validation and the routing to you.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.