You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: apps/website/content/blog/2026-05-28-human-in-the-loop-langgraph-agents-in-angular.mdx
+28-11Lines changed: 28 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,7 +11,8 @@ This is how to pause a LangGraph agent in Angular for human approval before it r
11
11
12
12
The example is a refund agent: it drafts a refund, then stops and asks an operator to approve, edit, or cancel before any charge is reversed.
13
13
14
-
Everything below is running code from the cockpit example at `cockpit/langgraph/interrupts`. Clone the repo, run `nx serve cockpit-langgraph-interrupts-angular`, and follow along.
14
+
Everything below is running code from the cockpit example at `cockpit/langgraph/interrupts`.
15
+
Clone the repo, run `nx serve cockpit-langgraph-interrupts-angular`, and follow along.
15
16
16
17
<divstyle={{ marginTop: 40, marginBottom: 44 }}>
17
18
<CardGroupcols={2}>
@@ -32,9 +33,13 @@ Everything below is running code from the cockpit example at `cockpit/langgraph/
32
33
33
34
## When should you use an interrupt?
34
35
35
-
Most tool calls don't need approval. Reads, searches, and lookups can run unattended. Reach for an interrupt when a tool does something the operator wouldn't want to undo by hand: moves money, sends a customer-facing message, deletes a record, or triggers a deploy.
36
+
Most tool calls do not need approval.
37
+
Reads, searches, and lookups can run unattended.
38
+
Reach for an interrupt when a tool does something the operator would not want to undo by hand: moves money, sends a customer-facing message, deletes a record, or triggers a deploy.
36
39
37
-
Two practical reasons: it caps the cost of a misfiring agent looping over a write API, and it gives the operator a checkpoint to catch a wrong action before it lands.
40
+
For me, there are two practical reasons.
41
+
It caps the cost of a misfiring agent looping over a write API.
42
+
And it gives the operator a checkpoint to catch a wrong action before it lands.
38
43
39
44
## The architecture
40
45
@@ -44,7 +49,8 @@ Three pieces:
44
49
-**`@threadplane/langgraph` adapter.** Surfaces the pending interrupt on an `agent.interrupt()` signal. `agent.submit({ resume })` writes a value back to the paused graph.
45
50
-**`@threadplane/chat` UI.**`<chat-approval-card>` reads the pending interrupt, opens a native `<dialog>` modal, and emits `'approve' | 'edit' | 'cancel'`.
46
51
47
-
The LangGraph node doesn't know how the UI renders, and the Angular component doesn't know which graph it's paused inside. That lets you reuse one approval dialog across multiple agents.
52
+
The LangGraph node does not know how the UI renders, and the Angular component does not know which graph it is paused inside.
53
+
That lets you reuse one approval dialog across multiple agents.
48
54
49
55
<figure>
50
56
<imgsrc="/blog/2026-05-28-human-in-the-loop-langgraph-agents-in-angular/1.png"alt="The refund agent's welcome screen with two suggestion chips: 'Refund a duplicate charge' and 'Refund a chargeback.'"width="1280"height="800" />
@@ -58,7 +64,8 @@ Three files.
58
64
<Steps>
59
65
<Steptitle="The LangGraph node">
60
66
61
-
A structured-output call populates the fields the approval card displays. Then `request_approval` pauses with `interrupt()`:
67
+
A structured-output call populates the fields the approval card displays.
68
+
Then `request_approval` pauses with `interrupt()`:
`interrupt()` pauses the graph and persists its payload. The graph stays paused until `agent.submit({ resume })` runs against the same thread — and that value becomes the return value of `interrupt()` when the node re-executes. That's why `request_approval` can branch on `decision["approved"]` and pick up an edited `amount`.
110
+
`interrupt()` pauses the graph and persists its payload.
111
+
The graph stays paused until `agent.submit({ resume })` runs against the same thread — and that value becomes the return value of `interrupt()` when the node re-executes.
112
+
That is why `request_approval` can branch on `decision["approved"]` and pick up an edited `amount`.
104
113
105
114
</Step>
106
115
<Steptitle="Wire the providers">
@@ -167,9 +176,13 @@ export class InterruptsComponent {
167
176
}
168
177
```
169
178
170
-
`<chat-approval-card>` reads `agent.interrupt()`, matches the `kind` you pass to `matchKind`, opens a native `<dialog>`, and emits an action on each button click. The body is a content-projected template — render whatever fits the payload.
179
+
`<chat-approval-card>` reads `agent.interrupt()`, matches the `kind` you pass to `matchKind`, opens a native `<dialog>`, and emits an action on each button click.
180
+
The body is a content-projected template — render whatever fits the payload.
171
181
172
-
Approve and Cancel are terminal: they resolve the interrupt and close the dialog. Edit is not. Clicking Edit leaves the dialog open so you can reveal an inline editor and submit the resume yourself. That distinction lives in the composition.
182
+
Approve and Cancel are terminal: they resolve the interrupt and close the dialog.
183
+
Edit is not.
184
+
Clicking Edit leaves the dialog open so you can reveal an inline editor and submit the resume yourself.
185
+
That distinction lives in the composition.
173
186
174
187
</Step>
175
188
</Steps>
@@ -191,7 +204,8 @@ Approve and Cancel are terminal: they resolve the interrupt and close the dialog
9. The graph continues to `issue_refund` and finishes.
193
206
194
-
It's one thread and one persisted state. If the operator closes the tab and returns later, the interrupt is still pending.
207
+
It is one thread and one persisted state.
208
+
If the operator closes the tab and returns later, the interrupt is still pending.
195
209
196
210
<figure>
197
211
<imgsrc="/blog/2026-05-28-human-in-the-loop-langgraph-agents-in-angular/3.png"alt="The chat after approval, showing the agent's draft summary and a confirmation: 'Refund of $47.50 issued to cus_a8x2k. Refund ID: re_demo__a8x2k.'"width="1280"height="800" />
@@ -202,7 +216,9 @@ It's one thread and one persisted state. If the operator closes the tab and retu
202
216
203
217
### Idempotency
204
218
205
-
`interrupt()` re-executes the node on resume. Side effects before the call have already run; side effects after run again on resume. Put the write (the Stripe `refund.create`) on the resumed side, and pass an idempotency key through state so a double-click doesn't issue two refunds.
219
+
`interrupt()` re-executes the node on resume.
220
+
Side effects before the call have already run; side effects after run again on resume.
221
+
Put the write (the Stripe `refund.create`) on the resumed side, and pass an idempotency key through state so a double-click does not issue two refunds.
Interrupt on writes the operator wouldn't want to undo by hand — money movement, customer-facing messages, destructive deletes. If you can undo it with a script in under a minute, let the agent run.
241
+
My rule is simple: interrupt on writes the operator would not want to undo by hand — money movement, customer-facing messages, destructive deletes.
242
+
If you can undo it with a script in under a minute, let the agent run.
0 commit comments