Human-in-the-Loop
Quick path: new to the API? Go from an API key to your first successful call in the Quickstart (~5 minutes), then come back here for the approval workflow. By default, the AI applies changes immediately. Setapproval_mode to "ask_every_time" to review changes before they take effect.
HITL requires the async workflow (/v1/chat/async) because the job pauses to wait for your approval.
This guide covers UI-driven approval (a human reviewing in your product’s interface). If your AI agent is the one deciding whether to approve proposed changes (no human in the loop), see Agent Tool Integration → Approval modes. For batch / server-side workflows that auto-approve every change, see Server Integration and pass
approval_mode: "approve_all" instead.Revert vs. pending approval. While a session is
awaiting_approval (one or more proposed changes pending review), calling revert returns 409. Resolve the pending approvals first — accept or deny each change, or cancel the in-flight job via POST /v1/jobs/{job_id}/cancel — then retry revert.Recommended UX pattern
Default your UI to auto-apply; expose Review Mode as an opt-in toggle. The simplest, least-friction integration is to sendapproval_mode: "approve_all" by default and put a small in-UI toggle — one control, clearly labelled — that the user can flip on when they want to review each change before it lands. When the toggle is on, your code switches to approval_mode: "ask_every_time" and renders the proposed-change UI (chat-side card, inline editor overlay, or native track-changes — see below).
This is the pattern the SuperDocs web app at use.superdocs.app uses, and it’s what most users expect:
- Auto-apply on by default — for 90% of edits (rewording, formatting, small additions), users don’t want to approve each change. They want the AI to act, then Cmd-Z if they disagree.
- Review Mode as an explicit opt-in — when the user is working on something high-stakes (a contract clause, a regulatory filing, a legal letter) they flip the toggle ON and approve each change explicitly. Persist the toggle state in
localStorageso it survives page reload. - No hidden state — both modes should be plainly visible in the UI. Don’t bury Review Mode in a settings modal.
ask_every_time without a toggle is valid for high-stakes products (contracts, medical records, court filings) where every change must be reviewed, but expect higher friction in casual editing flows. If in doubt, start with auto-apply by default and a toggle — you can always reverse it later.
End-to-end workflow
1. Send a request with approval mode
2. Poll for approval status
status is "awaiting_approval", check metadata.pending_changes:
3. Approve or deny changes
Approve a single change:4. Continue polling
After approval, the job resumes processing. Poll untilstatus is "completed" to get the final result.
If you deny a change with feedback, the AI receives your feedback and may propose a revised change. Your polling loop should handle multiple rounds of
awaiting_approval — not just one.Understanding proposed changes
Operation types
Each proposed change has anoperation field that determines what the AI wants to do and which fields are populated:
Proposed edits to page headers, footers, footnote/endnote bodies, and comments use these same shapes: each arrives as an ordinary entry (its
chunk_id targets the part’s block) in the same batch as any body edits from the turn, so one turn can propose a body change and a header change and the user approves each independently. There is no separate approval track for them.
Building a diff view
To show users what the AI wants to change, compareold_html and new_html:
- For
edit: Use a diff library (likediff-match-patchorjsdiff) to highlight additions and removals betweenold_htmlandnew_html. Show a before/after view or inline diff. - For
create: Displaynew_htmlwith a visual indicator that this is a new section being added (e.g., a green border or “New section” label). Theinsert_after_chunk_idvalue corresponds to adata-chunk-idattribute on an element in the document HTML — find that element and insert the new content after it. - For
delete: Displayold_htmlwith a visual indicator that this section will be removed (e.g., red strikethrough or “Will be deleted” label).
ai_explanation alongside the diff — it tells the user why the AI proposed the change.
Rendering diffs inline in your editor
Three patterns for where the diff appears, in order of visual weight:Pattern 1 — Side-by-side card in your chat panel
Render a card per pending change withold_html and new_html shown as two adjacent panels (red background for old, green background for new) and Approve / Deny buttons underneath. The card lives in your chat sidebar; the editor document is unaffected until the user approves. This is the simplest pattern and works with any editor. See the JavaScript example for a working HITL flow that produces this UI.
Pattern 2 — Inline overlay in the editor (Cursor-style)
The proposed edit appears directly inside the document — the affected block gets a coloured outline, the word-level diff renders inside it (red strikethrough for removed, green highlight for added), and Approve / Deny buttons float in the top-right corner of the block. This is the most accurate visual representation of what the AI wants to change and is the pattern most developer-audience apps converge on. For ProseMirror-based editors (ProseMirror, TipTap, BlockNote, Remirror, Atlaskit), use the editor’s native decoration system. The plugin below takes individual change objects (the entries of aproposed_change_batch event’s changes array), builds a DecorationSet keyed by data-chunk-id, and dispatches window-level custom events when the user clicks Approve or Deny.
- Register
proposedChangeDecoration()in the plugin array when you buildEditorState. - On every
proposed_change_batchSSE event in the chat panel, loop over the parsedchangesarray and calladdProposedChange(view, change)for each one. - Subscribe to
window.addEventListener("diff-action", ...)in the chat panel to catch Accept / Deny clicks. Post the decision to/v1/chat/{session_id}/approveand callremoveProposedChange(view, change_id). - On every
finalSSE event, callclearProposedChanges(view)before applying the new HTML — the editor is about to re-render anyway.
Pattern 3 — Native track-changes UI
If your editor already supports track-changes (CKEditor 5 with the Track Changes feature, TinyMCE Premium, etc.), map each proposed change (each entry of theproposed_change_batch event’s changes array) to a tracked suggestion in the editor’s native model. The user then approves or rejects via the editor’s built-in UI. Consult your editor’s track-changes API docs for the specific mapping — the SuperDocs side is identical to Pattern 1 or 2.
Batch changes
When the AI proposes multiple changes at once (common for sweeping edits across many sections), every change in the turn arrives together. The shape depends on whether you’re polling or streaming:- Polling: all changes appear in the
metadata.pending_changesarray on a single/v1/jobs/{job_id}poll. Each change carriesbatch_id(the first change’schange_id) andbatch_total(the count) for convenience. - Streaming (recommended): a single
proposed_change_batchSSE event carries the full batch as achangesarray. See Streaming guide →proposed_change_batch.
Grouping a batch by document
In multi-document sessions, every change in the batch carries thedocument_id it belongs to. Group the review list per document (one sub-section per document, with its own accept-all control) — a flat list of 40 changes spanning three documents is hard to reason about, the same 40 grouped under three document headers is easy. Note that a turn can also create a new document even in review mode (creation applies immediately; you’ll see it via the documents_changed event) — see SSE Streaming.
Very large batches
A whole-document instruction (“tighten every section”) can propose hundreds of changes in one batch. Two practical rules from running this at scale: virtualize the card list (render only visible rows — hundreds of side-by-side HTML diffs in the DOM at once will lock the tab), and lead with the summary (“312 changes across 5 sections types — Accept all / Review one by one”) rather than dropping the user straight into row 1 of 312.Recovering pending approvals after a reload
A pending review survives your client. If the user reloads the page (or your process restarts) while a job isawaiting_approval, the proposal is not lost, and the job keeps blocking new turns on the session until it’s resolved (the server auto-denies unattended reviews after about an hour). To restore the review UI on load:
- List the session’s jobs with
GET /v1/sessions/{session_id}/jobs(MCP:get_session_jobs). - Find the job with
status: "awaiting_approval"and branch onmetadata.awaiting_kindfirst: acontinue_promptpause is resumed with/continue, not/approve(see the flavours warning above). - Rebuild the review list from
metadata.pending_changes, exactly as you would from aproposed_change_batchevent. - Skip entries already decided before the reload:
metadata.pending_batch_decisionsmaps each decidedchange_idto{ "approved": true|false, "feedback": ... }. Render only the undecided remainder, and submit those decisions through the same/approvecalls as usual.
Alternative: SSE streaming workflow
The polling workflow above works but requires repeated API calls. For a real-time UI, use SSE to receive proposed changes as they’re generated:B2B / server-side integrators (organization
lce_ keys) that can’t hold an EventSource open — serverless functions, batch workers, agent runtimes — can long-poll instead of streaming. GET /v1/poll/{session_id}?since=<ISO-8601>&timeout=<seconds> (org keys only; timeout ≤ 1800s) holds the connection open until there’s a job update on the session, then returns it, so you get near-real-time progress without an SSE socket. Pass the last since timestamp on each call to avoid duplicate updates. Plain /v1/jobs/{job_id} polling (shown above) also works and is the simplest option.After calling
/approve, the AI resumes processing. If approved changes were applied, the SSE connection will eventually emit a final event with the updated document. If you denied with feedback, you may receive a new proposed_change_batch event as the AI tries again.
