State and reconciliation
The state file is how Lithos remembers what it created. Without it, every deploy would have to re-discover every asset on Roblox, and there is no generic API for "list everything I own." With it, a deploy is a fast, predictable diff.
What is in the state file
A versioned YAML document keyed by environment. In the current v7 format, each environment stores two things:
current— the resource graph from the latest successful deploy or undo.deployments— a short history of checkpoints, results, and journals used for recovery andlithos undo.
Concretely:
version: 7
environments:
dev:
current:
- id: experience_singleton
inputs:
experience:
groupId: ~
outputs:
experience:
assetId: 3296599132
startPlaceId: 8667346609
- id: placeFile_start
inputs:
placeFile:
filePath: game.rbxlx
fileHash: 0c3a…
outputs:
placeFile:
version: 2
deployments:
- id: deploy-1736370930123
kind: deploy
status: succeeded
startedAt: 2026-01-08T01:15:30Z
finishedAt: 2026-01-08T01:15:42Z
baseline: []
desired:
- id: experience_singleton
resulting:
- id: experience_singleton
journal:
- resourceId: experience_singleton
action: create
status: applied
summary: Applied create for experience_singletonYou will rarely edit this by hand. The important parts are:
fileHashis how Lithos decides if a place file changed. Editinggame.rbxlxflips the hash, which produces an update. Touching the file without changing bytes does not.outputs.assetIdis the canonical link between a Lithos resource and a real Roblox asset. Lose it and the next deploy will re-create the asset from scratch.deploymentsis the recovery trail. It records recent deploys and undos, including the baseline snapshot and a short journal of what changed.
Where the state file lives
There are two options. They store the same data; only the backend differs.
Local (the default)
.lithos-state.yml is written next to your lithos.yml. Commit it.
Local state is fine when:
- You are the only developer.
- You only deploy from one machine.
- You are happy to merge state-file conflicts in Git when collaborators deploy from different branches.
Remote (S3, R2)
State is written to a single object in S3 or Cloudflare R2. Multiple machines see the same state automatically.
Remote state is the right answer when:
- More than one person, machine, or CI job deploys.
- You want CI to be the only thing that deploys to
prod, with developers deploying todevfrom their laptops. - You don't want a state file in source control.
Setup: Remote state guide.
Lithos does not support "no state." Pick local or remote.
Drift, in one paragraph
Drift is when the live Roblox state and the Lithos state file disagree. Most often, someone changed something through Creator Hub or Studio. The live reconciliation pass detects drift for the resource types Lithos can verify cheaply and updates state before planning the diff. See The resource graph.
You can run the reconciliation pass on its own without applying anything:
lithos diff --liveThat prints the same diff a deploy would, against the reconciled state,
without prompting and without writing.
Concurrency: locks and compare-and-swap
Lithos serializes mutating operations (deploy, undo, destroy,
import) on a per-environment basis. Two concurrent runs against the same
environment will not race: one wins, the other fails fast with a
diagnostic.
This is implemented with two layers that live entirely inside the state document, so the protocol is identical for local files and for remote backends.
Compare-and-swap on every write
Every mutating save is compare-and-swap (CAS). When a command loads state, it captures a revision handle (a content hash, plus an optional backend ETag when available). When it tries to save, the store rejects the write if the handle no longer matches what is on disk or in the bucket.
If the conflict is on a different environment, Lithos automatically re-bases: it keeps the slice it just modified and merges in the other environment(s) from the latest version. Cross-environment runs therefore never block each other.
If the conflict is on the same environment, the save aborts with a hard error. The command exits non-zero without persisting.
For local state this is enforced by hashing the on-disk file plus a tmp + atomic-rename write. For S3/R2 it is best-effort (load-then-write); the authoritative protection in the multi-writer case is the in-document lock described next.
Environment-scoped locks
Before a mutating command touches anything, it acquires an advisory lock
for the target environment. The lock lives inside the state file (under a
top-level locks: map keyed by environment label) and carries:
- The owner id (hostname + pid + nanosecond timestamp).
- The host, PID, and operation name (
deploy,undo, …). - The time it was acquired and the last heartbeat.
A second lithos deploy --environment prod while a first one is still
running fails immediately with a diagnostic that names the holder and
points at lithos lock break. Long-running deploys refresh the heartbeat
on every progress write, so the lock stays fresh while real work is
happening.
Stale-lock recovery
If a run crashes, loses its network, or is killed, its lock will eventually go stale (heartbeat older than the TTL — 15 minutes by default). The next acquirer reclaims it automatically.
For faster recovery — for example, you know the CI job that crashed five minutes ago is gone for good and you don't want to wait for the TTL — use the CLI:
# Show every lock currently held, with staleness flagged.
lithos lock list
# Forcibly release a specific environment's lock.
lithos lock break --environment prodOnly break a lock after confirming no other Lithos process is still mutating that environment. Breaking the lock while a real deploy is in flight does not corrupt state — the running deploy's next CAS write will fail and the run will abort — but you will end up with a partial deploy and have to run again.
Legacy mantle.yml / .mantle-state.yml
Lithos reads both Mantle and Lithos config and state files. New writes use the Lithos names. See Migrating from Mantle.
Common state-file questions
Q: My teammate and I both deployed dev and now Git cannot merge the
state file.
Use remote state, or make one machine the only writer for that environment.
Q: Can I delete the state file and start over?
Yes, but Lithos will think nothing exists and try to re-create every
resource. The old assets stay on Roblox, orphaned, and you lose the rollback
checkpoints recorded in deployments. Better: use
lithos import to attach an existing experience to a fresh state file
(experimental).
Q: Can I edit the state file by hand? Only as a last resort, and always with a backup. Wrong asset IDs can make Lithos create duplicates, fail auth, or point at the wrong Roblox asset.