Checkpoints & review gates
Two mechanisms let you see and undo what an agent did, and they share one diff surface.
Checkpoints snapshot the workspace around every run and every chat turn, so any of them
has a diff — nothing to switch on; click the ± beside a run in the Runs sidebar. Review
gates are a node type you drop into a graph to make the run stop mid-flight and wait for
your approve or reject.
Checkpoints
Section titled “Checkpoints”A shadow git repository (<latchHome>/runs/checkpoints.git, with the workspace as its
work-tree) snapshots around every run: pre-run <runId> before it starts, run <runId>
after it finishes. Two consequences follow from the “shadow” part — your own repositories
are never touched by LatchAI’s undo history, and the workspace doesn’t have to be a git
repository at all for this to work. Commits are authored as latchai <latchai@local>, so
they are obvious for what they are.
Nothing about this is opt-in. Every workflow run and every chat turn begins with a
pre-run commit of whatever was outstanding and ends with a run <id> commit of what
changed, so “what did that do?” always has an answer. Because work items
live at workspace/work/ and agent memory at
workspace/memory/, a card an agent moved and a note it rewrote are in the same diff as
the files it edited.
Run diffs surface as checkpoint.created events carrying a short sha, a --stat summary
and the diff itself (capped at 100,000 characters), and the Runs sidebar grows a ± per
run that opens it.

Every run has a before and after.
A few boundaries are deliberate:
- Workspace only. Mounted project folders are not in the shadow repo — they usually
have their own git, and the git panel is the surface for
those. What gets checkpointed is what agents write into
workspace/: drafts, briefs, chat notes, dashboard and atlas exports, and agent memory. - Dependency trees and symlinked project roots are excluded —
node_modules/,*.log,.DS_Storeandprojects— because a diff drowning in ten thousand vendored files is not a review, and git’s output buffer agrees. - A run that changed nothing produces no checkpoint and no event, so the sidebar’s
±simply doesn’t appear.
When it can’t
Section titled “When it can’t”Two failure modes are worth knowing about, because both are quiet by design.
If the shadow repo cannot be created at all — no git on the path, an unwritable home —
checkpoints are disabled for that session. The daemon logs checkpoints disabled: …
once at boot and everything else keeps working; you simply get no ± and no gate diffs.
Git in the shadow repo runs synchronously on the daemon’s only thread, so it is bounded at
60 seconds. A git add -A over a very large workspace on a cold cache is legitimately
slow, and the generous limit is deliberate — but if it does expire, that run loses its
checkpoint rather than the engine losing its thread.
Reverting
Section titled “Reverting”The overlay’s ⟲ Revert changes button applies an inverse commit in the shadow repo
(git revert --no-edit <sha>), which undoes that run’s file changes without rewriting
history.
The button only appears when the overlay was opened with a revertable sha, and today that
means chat turns — the ± Review changes affordance in the
chat panel. A run diff opened from the
Runs sidebar is read-only for the same reason: it is opened without one.
The endpoint itself is not chat-specific. POST /api/checkpoints/revert with
{"sha": "…"} reverts any checkpoint:
curl -X POST http://127.0.0.1:7777/api/checkpoints/revert \ -H 'content-type: application/json' \ -d '{"sha":"9f2c4d81aa"}'Review gates
Section titled “Review gates”The review_gate node type pauses a run where you put it. It snapshots the mid-run diff,
emits a gate.waiting event carrying the --stat and the diff (the same 100,000-character
cap), and blocks until the gate is resolved. The workbench auto-opens a modal review
overlay — a colored unified diff, an optional note, and Approve/Reject.
Approving continues the run, and the note becomes the node’s output, so downstream nodes can read what the reviewer said. Rejecting fails the node, and by default that fails the run.

The run is blocked until someone decides.
A pending gate always outranks any other diff you have open, and — unlike a browse-only diff — the gate overlay has no close button and does not dismiss on a click outside. That is on purpose: a paused run wants a decision, and burying the question behind a diff somebody opened to read is how a pipeline stalls until morning.
Placing one
Section titled “Placing one”A gate is an ordinary node with no config of its own — everything it needs it gets from where you put it in the graph:
{ "formatVersion": 2, "id": "release-notes", "name": "Release notes", "nodes": [ { "id": "trigger", "type": "manual_trigger", "position": { "x": 60, "y": 160 }, "config": {} }, { "id": "draft", "type": "agent", "position": { "x": 320, "y": 160 }, "config": { "agent": "release-writer", "prompt": "Draft the notes into out/notes.md." } }, { "id": "review", "type": "review_gate", "position": { "x": 580, "y": 160 }, "config": {} }, { "id": "publish", "type": "agent", "position": { "x": 840, "y": 160 }, "config": { "agent": "publisher", "prompt": "Publish out/notes.md. The reviewer said: {{review}}" } } ], "edges": [ { "from": "trigger", "to": "draft" }, { "from": "draft", "to": "review" }, { "from": "review", "to": "publish" } ]}The diff the reviewer sees is everything the run has changed so far — from the run’s starting commit to the working tree at this instant — not just the immediately preceding node. That is usually what you want: the question a gate asks is “is this run’s work acceptable”, not “did that one step behave”.
What the reviewer’s note is for
Section titled “What the reviewer’s note is for”{{review}} resolves to approved when the note is empty, and approved: <your note>
when it isn’t. That makes the note a real input rather than an audit comment: “approved:
tighten the headline before publishing” is something the next agent can act on.
Rejecting fails the node with review rejected: <note>, which by default fails the run —
the run.failed event carries the reason, so the log records why a nightly job stopped.
Either way a gate.resolved event records the decision and the note, so the log holds the
answer as well as the question.
A gate obeys the error policy
Section titled “A gate obeys the error policy”review_gate takes no config, but it is an ordinary node, so the per-node
error policy applies to it like any other:
retryre-runs the gate node, which means it asks again — a freshgate.waitingwith a fresh diff.timeoutMsbounds how long one attempt may block. Without it a gate waits as long as the engine does, which is the default and usually what you want for an overnight approval.onError: "continue"turns a rejection into a handled outcome: the node’s output becomes{ ok: false, error: "review rejected: …", attempts: 1 }and the run carries on.onError: "branch"does the same and routes the run down the node’swhen: "error"edges — which is how you build “if the reviewer says no, file a work item instead of dying”.
A stop always wins over all of it: pressing ⏹ on a run blocked at a gate unblocks the wait
with run stopped while waiting for review, and the run’s terminal event is run.stopped.
Without that a stopped run would sit forever on a question nobody was going to answer.
Under concurrency above 1, a gate blocks only its own branch: other branches keep
running while the reviewer decides.
Resolving without the UI
Section titled “Resolving without the UI”Gates are resolved over HTTP, which is what the overlay calls:
curl -X POST http://127.0.0.1:7777/api/gates/resolve \ -H 'content-type: application/json' \ -d '{"runId":"run-1757390512-3","nodeId":"review","approved":true,"note":"ship it"}'GET /api/gates answers with two lists:
{ "pending": ["run-1757390512-3:review"], "orphaned": [{ "key": "run-1757301004-8:review", "runId": "run-1757301004-8", "nodeId": "review", "at": 1757301180000 }]}pending is the keys with a live waiter — the ones resolve can actually answer. A key
that isn’t there comes back 404 (no such pending gate), so a double-approve is a clear
error rather than a silent no-op.
orphaned is the other half, and the reason gates survive a restart.
The same surface everywhere
Section titled “The same surface everywhere”Chat turns are checkpointed too, and working-changes review points the identical overlay at your git diff. Whether the change came from a scheduled run, a chat turn, or your own uncommitted work, “show me the diff” looks the same.