How the Action Simulation Model Works in Cloudflare OS Gatekeepers
Cloudflare OS gatekeepers use an action simulation model that lets agents read external service state as if all pending writes have already been applied, even while those writes await human approval.
The action simulation model enables a seamless developer experience within the cloudflare/cloudflare-os repository. By separating durable action storage from pure-functional state overlays, gatekeepers can queue destructive operations without blocking execution, presenting a consistent "what-if" view to calling code immediately after a write request.
Core Components of the Simulation Architecture
The simulation layer is implemented across three primary modules in the gatekeeper-kit package, each responsible for a distinct phase of the action lifecycle.
Action Journal (action-journal.ts)
The Action Journal serves as the durable storage layer for every queued operation. Located at packages/gatekeeper-kit/src/action-journal.ts, this module records the action ID, target resource, payload, and current status (pending, approved, failed). It provides the authoritative source of truth for all changes that have been requested but not yet committed to the external API.
Simulation Records (simulation.ts)
The Simulation module at packages/gatekeeper-kit/src/simulation.ts constructs immutable, ordered snapshots derived from the journal. These simulation records are indexed by their target identifier—such as a GitHub issue number, Notion page ID, or Spotify playlist—and contain the partial payload representing the pending change. The records are sorted by journal ID to ensure deterministic ordering when multiple actions target the same resource.
Overlay Helper
The overlay helper provides pure-functional utilities to merge simulation records onto live API data. The primary entry point, applySimulationOverlay, accepts the real external data and the current simulation records, then returns a frozen object representing the combined state. This design prevents side effects and accidental mutation across concurrent read operations.
Execution Flow of the Action Simulation Model
The simulation lifecycle follows five distinct stages, from initial request through final resolution.
1. Queueing Actions with Simulation Delivery
When a gatekeeper receives a write request—such as adding a track to a Spotify playlist—it creates an action and persists it to the Action Journal. The action’s delivery field is explicitly set to "continue-with-simulation", allowing the calling agent to proceed immediately without waiting for human approval.
import { queueAction } from "@gatekeeper-kit/actions";
async function addTrack(playlistId: string, track: Track) {
await queueAction({
target: { type: "playlist", id: playlistId },
payload: { op: "add", track },
delivery: "continue-with-simulation",
});
// Caller can immediately read the playlist and see the new track
}
2. Building the Simulation View
The getSimulationView function in packages/gatekeeper-kit/src/simulation.ts reads all pending actions from the journal, sorts them by their unique ID, and constructs the immutable SimulationRecords collection. Each record maps a target identifier to its pending transformation, creating a complete overlay dataset without modifying external services.
3. Overlaying Pending Changes on Reads
When the gatekeeper performs a subsequent read operation, it first fetches the real data from the external API, then invokes the overlay helper to apply pending changes. The applySimulationOverlay function walks the simulation records, applies adds, updates, and deletes to a fresh copy of the real data, and returns the simulated view.
import { getSimulationView, applySimulationOverlay } from "@gatekeeper-kit/simulation";
import { fetchPlaylist } from "./spotify-api";
async function listTracks(playlistId: string) {
const real = await fetchPlaylist(playlistId); // Live API call
const sim = await getSimulationView(playlistId); // Pending actions
const view = applySimulationOverlay(real, sim); // Merge changes
return view.tracks; // Includes simulated additions
}
4. Human Approval and State Resolution
Queued actions remain in the journal until an administrator approves or rejects them via the approveAction utility. Upon approval, the gatekeeper executes the real request against the external API and discards the corresponding simulation record. If rejected, the simulation record is removed without executing the external call, causing subsequent reads to revert to the original state.
import { approveAction } from "@gatekeeper-kit/actions";
async function approvePending(actionId: string) {
await approveAction(actionId); // Executes real request, removes simulation entry
}
Consistency Guarantees and Safety
The action simulation model in cloudflare-os provides three critical safety properties:
- Deterministic Ordering: Actions are sorted by journal ID before overlay application, ensuring repeatable results regardless of retrieval timing.
- Immutability: The overlay helper returns frozen objects, preventing accidental mutations that could leak simulated state into production data.
- Side-Effect Isolation: Simulation logic remains pure-functional; no network requests or state changes occur during the overlay phase.
Summary
- The action simulation model enables immediate read-after-write consistency without waiting for human approval.
- The Action Journal at
packages/gatekeeper-kit/src/action-journal.tsprovides durable storage for pending operations. - Simulation records in
packages/gatekeeper-kit/src/simulation.tscreate immutable, ordered snapshots of pending changes indexed by target resource. - The
applySimulationOverlayfunction merges pending changes onto live API data using pure-functional transformations. - Actions specify
delivery: "continue-with-simulation"to opt into the simulation workflow. - Approval via
approveActiontriggers the real external API call and removes the simulation overlay.
Frequently Asked Questions
What is the primary purpose of the action simulation model in Cloudflare OS?
The action simulation model allows gatekeepers to queue sensitive write operations for human approval while immediately presenting the expected state to calling agents. This bridges the gap between asynchronous approval workflows and synchronous developer expectations, ensuring code can read back changes it just wrote without encountering stale data.
How does the applySimulationOverlay function handle conflicting actions?
The function processes simulation records in strict journal ID order, applying each pending change sequentially to a fresh copy of the real data. Because the records are immutable and the overlay is deterministic, conflicts resolve predictably based on action ordering, with later actions overriding earlier ones when targeting the same resource field.
Where are the simulation records stored and how long do they persist?
Simulation records exist only in memory during the overlay computation; they are derived on-demand from the Action Journal stored in packages/gatekeeper-kit/src/action-journal.ts. Records persist in the journal until explicitly removed—either through the approveAction flow (which commits the change externally) or via rejection (which discards the pending operation).
Which files should developers reference when implementing custom gatekeepers with simulation support?
Developers should examine packages/gatekeeper-kit/src/simulation.ts for the overlay logic and record types, packages/gatekeeper-kit/src/action-journal.ts for durable action storage, and packages/gatekeeper-kit/__tests__/simulation.test.ts for unit tests demonstrating expected overlay behavior. Gatekeeper-specific implementations in packages like gatekeeper-spotify and gatekeeper-github provide concrete usage patterns.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →