Skip to main content

Deploy state

Every deploy records a map from each code resource (kind:slug) to the real uuid Cargo assigned it, plus its outputs and a content hash:
That map is the authoritative link from code to deployed infra. Connectors and models are slug-addressable and self-heal, but plays, agents, tools, files and MCP servers have no slug — their stored uuid is the only way to re-find them. It lives in your workspace, not in the repo. The repo commits a pointer to it:
cargo-ai project init creates the state and writes that cargo.state.json as part of the scaffold, so the pointer is in the first commit rather than appearing in whichever teammate deploys first. It records only uuids, hashes and outputs — never secret values.
Commit cargo.state.json. Without the uuid, a fresh checkout has no way to find the state it belongs to. Deleting it does not silently make a new one — a deploy stops and tells you to run cargo-ai project state list and cargo-ai project state bind <uuid>, because creating a replacement would orphan everything the old state tracks.

One state per repo, at most 10 per workspace

A state belongs to a git repo, not to a workspace. Each project init creates its own and commits its own uuid, so a workspace holding five GTM repos holds five states and they never share a map. A workspace may hold 10 live states; creating an eleventh is refused until one is removed.

Managing it

Projects created before states moved to the workspace

Their cargo.state.json holds the resource map itself, and it keeps working: plan, deploy and everything else read and write that file exactly as they did. The file is the state, so losing it has no recovery path but git. Moving one into the workspace is a single command, run once by one person:
Nothing migrates a project on its own. The pointer has to reach your teammates through git — if a deploy moved the map for whoever ran it first, everyone still on the old file would carry on deploying from a state that had already moved.
There is no flag to choose between the two: the shape of the file decides. Resources present means the file holds the state, a stateUuid means the workspace does. Anything that let you contradict it would only ever deploy a second copy of everything the pointer already tracks.

Lock and sibling files

A deploy takes a lock on the state for its duration, so two runs cannot interleave writes. A lock is released when the run ends, and one left by a run that died expires after an hour; --force steals one you believe is stale. Cargo also writes sibling files next to cargo.state.json — a cargo.state.bak.json (local rollback snapshot), a cargo.state.audit.jsonl (run log), a cargo.state.lock (local backend only), and a cargo.state.cache.json (the last blob read from the workspace, so project info can report what is deployed without a round trip). Only cargo.state.json is committed; git-ignore the rest:

Drift

Cargo compares your code against state. It can also compare against the live workspace to catch changes made outside the project (e.g. someone edits an agent in the Cargo UI, or deletes a folder).

Detect (read-only)

Re-reads every state-tracked resource and reports each as unchanged, modified externally, or deleted externally. It changes nothing. A modified resource also names the fields the edit landed in, so you can see whether re-applying your code over it would lose anything:
The fields are names only — state stores a hash per field, never the value, so nothing a resource holds (a connector’s credentials, say) is written to the file you commit. A resource last deployed by an older CLI has no per-field baseline and reports as modified externally with no fields until its next deploy.

Correct

Folds the drift into the plan before applying:
  • Modified externally → re-applies your code over it (code wins).
  • Deleted externally → the deploy stops and asks you to re-run with --recreate-deleted before it will recreate anything — a deliberate gate so a resource someone removed on purpose doesn’t silently come back.
Drift is measured against the state captured at the last deploy, not guessed from code — so it reflects real changes to the live resource. A transient read error is reported as unknown, never as a deletion, so a network blip can’t trigger a mass re-create.

Code and UI round-trips

Resources deployed from code remain fully editable in the Cargo UI — but the project never reads those edits back into your files. What happens to UI work on the next deploy follows directly from the hash model:
  • A plain project deploy compares code against state, not against the live workspace. If you haven’t changed a resource in code, it plans as = unchanged and is skipped — UI edits to it survive every deploy.
  • The moment you change that resource in code, the next deploy pushes the full code spec and overwrites the resource — including anything changed in the UI since.
  • project deploy --refresh re-applies code over every externally-modified resource, whether or not its code changed. Reach for it when you want to reset to what the repo says; avoid it while UI work is in flight.
The practical rule: pick one owner per resource. Starting a workflow in code and finishing canvas-only parts in the UI works — but from then on treat the UI as that resource’s source of truth: leave its code definition alone, or port the canvas changes back into code before touching it again. Run cargo-ai project refresh before deploying to see exactly which resources have diverged.

Secrets and drift

Because secret() values are excluded from the content hash, rotating a secret does not show as drift and a plain deploy won’t push the new value (nothing changed). To roll a rotated secret, re-apply the resource (make any other change, or use --refresh). Use env() instead if you want a config value tracked in the hash. See Secrets & environments for the full secret() vs env() rules and how to deploy the same code to a second workspace.