There's a class of bug that only shows up in knowledge management systems: the invisible overwrite. You open a wiki page in your editor, make a small tweak to clarify a sentence, and save. Three weeks later a query that used to return the right answer comes back empty. The page is active. The source file hasn't changed. Everything looks fine from the outside. But somewhere in that "small tweak," you deleted a paragraph that the search index depended on.
You have no diff. No snapshot. No way back except your memory of what it used to say, which is already suspect, because if it were reliable you would have caught the problem sooner.
This is the problem we built Content Snapshots to solve in Synthadoc v1.2.0.
Why a Lifecycle System Isn't Enough on Its Own
Synthadoc already had a detailed audit trail before v1.2.0. Every lifecycle transition — draft to active, active to stale, active to archived — was recorded in the lifecycle_events table with a timestamp, a trigger source, a reason string, and the before/after states. You could reconstruct when every state change happened and what caused it.
What the audit log couldn't answer was: what did the page say at that moment?
The distinction matters more than it sounds. A wiki page doesn't just have a state; it has content. And the content can change independently of the state. You can promote a page from draft to active on Monday, edit it in Obsidian on Tuesday, and have the lint agent mark it stale on Wednesday, but if the Tuesday edit introduced a subtle error, the Wednesday stale flag won't tell you anything about what went wrong. You need a record of what the page contained at each of those moments.
The audit infrastructure was already there — the lifecycle_events table, the TriggerSource enum, the log queries. What we were missing was a content_snapshot column to store the page body alongside each event. That was the architectural gap.
Architecture: Three Triggers, One Table
The design decision we landed on was to capture snapshots at the points in the lifecycle where content is most likely to have meaning: immediately before or at the moment of a consequential event. Not on every save, not on a timer, but at the moments that already have semantic weight in the audit trail.
Three distinct triggers produce snapshots:
Manual lifecycle transitions. Whenever the user calls lifecycle activate, lifecycle archive, or lifecycle restore — through the CLI, the Obsidian Lifecycle modal, the REST API, or the MCP tool — the current page body is captured into the event record. This gives you a snapshot of exactly what the page contained when you decided it was ready to go live, or when you chose to take it offline.
Lint-driven state changes. When the lint agent transitions a page automatically — promoting a draft to active, marking an active page stale because its source hash changed, auto-resolving a contradiction — the snapshot fires there too. For the auto-resolve case in particular, this captures the post-resolution body that was just written, which is the version you'd want to roll back to if the resolution turns out to be wrong.
Direct edits in Obsidian. When you edit a wiki page directly in the Obsidian vault, the plugin's file watcher fires after a two-second debounce. This trigger is the only one with deduplication: snapshot_if_changed() compares the incoming content to the last stored snapshot, and only writes a new row when the content actually differs. Since the file watcher fires on every keystroke-batch, without dedup you'd generate hundreds of identical rows for a single editing session.
The state flow looks like this:

For the Obsidian edit trigger, from_state and to_state are both set to the current page state, since no lifecycle transition is occurring. The snapshot is a pure content checkpoint.
Storage is roughly 3 KB per snapshot at typical wiki page sizes. The existing purge_lifecycle_events() endpoint handles cleanup, the --before and --keep-latest options remove event rows along with their embedded snapshots.
What Was Harder Than Expected
The three-trigger model looks clean in the design doc. Getting the semantics right in the implementation was less clean.
The rollback problem. Rollback sounds simple: take the page body from snapshot N and write it to disk. But what if you roll back to a snapshot that was itself wrong, and need to undo the rollback? We made rollback undoable by always recording the current body as a new snapshot before writing the target. That means index 1 is always the most recent snapshot, and rolling back to index 2 creates a new index 1 that captures what you just had. The operation never changes page_states or calls set_page_state , it's a content operation, not a lifecycle operation.
The dedup boundary. The two-second debounce on the Obsidian file watcher prevents the snapshot log from filling up with per-keystroke noise. But two seconds isn't always enough if the user pauses mid-edit. We added a content equality check on top of the debounce — snapshot_if_changed() — so even if multiple debounced events fire for the same editing session, only one row gets written if the content hasn't changed between fires. The check is a simple string equality comparison; at typical page sizes this is negligible overhead.
The ingest carve-out. One decision that caused more discussion than expected: ingest-agent transitions do not produce snapshots. This is intentional. The ingest agent writes the page body from scratch based on the source document; capturing a snapshot at that point would just duplicate the ingested content. The meaningful history is what humans wrote or edited, not what the ingest agent synthesised. Lint-driven transitions — where the agent may overwrite human edits to resolve a contradiction — do snapshot, because that's the case where you might want to go back.
Use Case Walkthrough
Here's the scenario this was designed for. You have an active wiki page on a topic that your team references frequently. A colleague opens the page in Obsidian, adds a paragraph, and saves. The edit looks fine. Two days later the lint agent runs, sees that the source file hash has changed, and marks the page stale.
Before v1.2.0, you'd have an audit trail entry saying "stale, reason: source hash mismatch" but no way to see what the page contained when the colleague edited it versus what it contained when it went stale. You'd have to diff the current file against the source document by hand.
Now the audit history for that page shows three entries: the original activation (with the body as it was promoted), the colleague's Obsidian edit (captured by snapshot_if_changed(), a content-only checkpoint), and the lint-driven stale transition (captured with the post-lint body). You can open a diff between any two of them in the Obsidian Content Snapshots tab and immediately see what changed.
If the colleague's edit inadvertently removed something important, you can roll back to the activation snapshot. The rollback records the current body first, so if you change your mind, rolling back to index 1 gives you back the stale version.

CLI Commands
Content Snapshots are accessible from the command line without the Obsidian plugin.
List all snapshots for a page, with index, timestamp, transition, and content size:
synthadoc lifecycle history alan-turing
View the content of a specific snapshot (index 1 is always the newest):
synthadoc lifecycle history alan-turing --index 2 --show-content
Roll back to a previous snapshot. The current body is saved as a new snapshot first, so the rollback itself is undoable:
synthadoc lifecycle rollback alan-turing --index 2 --reason "reverting bad edit"
Remove old snapshot data while keeping the most recent N events per page:
synthadoc audit lifecycle purge --keep-latest 5
Diff between snapshots is available in the Obsidian Content Snapshots tab, select any two entries in the history list to compare them side by side. The CLI surfaces the raw snapshot content via --show-content; piping two snapshots through your preferred diff tool achieves the same result.

Why This Matters
The content snapshot architecture is, at its core, a discipline about what you need to know when things go wrong. Most knowledge management systems are optimised for retrieval — for answering "what does this page say now?" Content Snapshots shifts some of that engineering toward answering "what did it say before?" and "which change caused the problem?"
The practical consequence is that a wiki built with Synthadoc behaves more like a version-controlled document than a database record. You get the full lifecycle audit — who promoted what and when — plus a point-in-time content record at every moment that mattered. You can reconstruct the editorial history of any page from activation to the present, view any snapshot's content, and roll back to any of them.
What we found in building this is that the rollback undoability was the right call even though it slightly complicates the storage model. A rollback that can't itself be undone is a footgun. The cost is an extra row per rollback operation; the benefit is that the snapshot log is always accurate and reversible.
Synthadoc v1.2.0 is available at github.com/axoviq-ai/synthadoc. The Content Snapshots feature works across all Synthadoc interfaces — CLI, Obsidian plugin, REST API, and MCP.
For a step-by-step walkthrough — including the Obsidian diff view and a live rollback — see the Snapshots and content recovery section of the Quick Start Guide.