Reverting changes
Every MCP mutation is recorded in mcp_changes, a per-workspace change feed. Your agent can list recent changes and revert any that fall within the 24-hour window.
The two tools
mcp.list_changes
mcp.list_changes({ limit?: number, cursor?: string })
→ {
changes: Array<{
id: string,
kind: string, // tool name, e.g. "workspace.update_brand"
primaryEntityKind: string,
primaryEntityId: string,
summary: string,
revertible: boolean,
revertibleUntil: ISO timestamp,
revertedAt?: ISO timestamp,
createdAt: ISO timestamp,
}>,
nextCursor?: string,
}
Returns the workspace's change feed, newest first. Includes both revertible changes and tombstone rows (revertible: false) for irreversible actions like sends.
mcp.revert_change
mcp.revert_change({ changeId: string })
→ { reverted: true, summary: string }
| { error: "not_revertible" | "expired" | "already_reverted" | ... }
Reverts the named change. Revert behavior depends on the kind:
- Create-style (
funnel.split.create,email.template.create,api_key.create): the created entity is archived/deleted. - Update-style (
workspace.update_brand,email.template.update,team.change_role): the prior state is restored frompreviousStatesnapshot. - Tombstone (
email.draft.send_now,billing.cancel_subscription,workspace.delete): cannot be reverted. The entry exists in the feed for audit/visibility only.
Tombstone rows
Some actions cannot be undone. Email sends leave the inbox; cancelled subscriptions trigger Stripe webhooks; deleted workspaces cascade-delete millions of rows. For these, mcp_changes still records the change (with revertible: false) so the agent's change feed stays complete — but mcp.revert_change will error with not_revertible.
| Action | Why tombstone |
|---|---|
email.draft.send_now | Emails left the building |
email.campaign.send_test | Test email sent |
billing.cancel_subscription / billing.resume_subscription / billing.change_plan | Real money flowed |
team.remove_member (post-accept) | The user already had access |
api_key.revoke | Secret destroyed |
tracking.site.delete / email.domain.delete / email.sender.delete | Cascade ripples through linked entities |
workspace.delete / workspace.transfer_ownership | Fundamentally one-way |
The 24-hour window
revertibleUntil is 24 hours after createdAt. After that, the change is read-only history. Tombstones never become revertible regardless of time.
Snapshot-and-restore for updates
Update-style changes write a JSON snapshot of the entity's prior state into mcp_changes.previousState. On revert, the snapshot is written back via tx.update(...).set(snapshot). This works for any single-entity update — workspace settings, email template body, sender display name, etc.
For multi-entity creates (a single funnel.split.create call creates a parent split row, multiple FunnelTrack rows, multiple FunnelStage rows, and may modify the funnel's canvasState), the revert handler knows to undo all of them via the autoCreatedEntities array.
UI awareness
The dashboard polls mcp.list_changes every 30 seconds. When new agent-driven changes appear:
- A toast notification shows "Agent made N change(s). Refresh to see latest."
- Clicking the toast refreshes the active page's data.
- Users can also browse the full feed in Settings → Developer → Activity alongside the per-call audit log.
This means if you and an agent edit the same template at the same time, you'll see the agent's change within 30 seconds and can choose to revert it before your edits land.
Agent etiquette
Good agents:
- Surface changes that touched the user's recent context (e.g. "I just updated workspace brand from #7c3aed to #ff5733 — say 'revert that' if you'd rather keep the old color")
- Offer revert proactively after destructive-feeling mutations
- Don't list every change in a chatty way; the feed is for audit, not narration
Related
- Safety tiers — what's revertible and what isn't.
- MCP — main MCP overview.