Skip to main content

Workspace Manifests

A manifest is a single YAML file that describes a Crewship workspace as data — agents, skills, credentials, MCP servers, and sidecar containers (Redis, Postgres, etc.). crewship apply --file manifest.yaml converges the live workspace toward the file’s declared state through the same REST API the UI uses; nothing skips RBAC, nothing writes to the database directly. This guide is the narrative tour. For per-field reference, see Configuration → Manifest Schema. For the apply / export CLI flags, see CLI → apply and CLI → export.

When to use it

One file = one workspace

Copy-paste a crew between teams, machines, or environments without re-clicking through the UI.

Git as source of truth

Commit your crews next to your code. Diff agent prompts. PR-review skill changes.

Reproducible setups

crewship apply is idempotent — re-running the same manifest produces the same workspace.

CI-friendly

Run from a pipeline with --yes --from-env to provision a fresh workspace per branch.

Mental model

The two kinds this guide tours: They are the workhorses, but not the whole vocabulary — the manifest system accepts 20 kinds (internal/manifest/schema.go), applied in dependency order by the same planner: Crew, Agent, Integration, Workspace, Project, Label, Milestone, WorkflowTemplate, TriageRule, RecurringIssue, SavedView, Routine, FeatureFlag, InstanceSetting, Recipe, CrewTemplate, Connector, Hook, Skill, Issue. examples/manifests/full-complete.yaml exercises most of them in one file; per-kind field reference lives in the manifest schema.

Your first manifest

Save this as code-review.crew.yaml:
Apply it:
Output:
--from-env reads ANTHROPIC_API_KEY and GH_TOKEN from the process environment. Skip it (or set --secrets-file env.list) and the credentials are created as PENDING slots that show up in the UI as “Needs value” with a CTA to fill them in.

Apply modes

Re-running apply is idempotent and convergent — the manifest is the source of truth, so resources missing from the live workspace get created, drifted ones updated, and resources that disappeared from the manifest get deleted.
Destructive operations (delete, replace) always prompt for confirmation unless --yes is passed. The plan is printed first so you can see exactly what will be mutated.
What sync does delete when missing from the manifest:
  • crews (per workspace bundle)
  • agents within each declared crew
  • agent skill bindings (agent_skills join)
  • agent credential bindings (agent_credentials join)
  • MCP servers on each declared crew
What sync does not delete (additive only — drop them through the UI or other CLI commands):
  • skills at workspace scope (often shared across multiple crews)
  • credentials themselves (their values may live elsewhere)

Credential safety

Manifests never carry secret values. The credentials: block declares slots — env, provider, type, label. Commit the manifest to git; the secrets stay out of it.
Values arrive at apply-time through one of three paths:
The third mode creates credentials with status=PENDING. Agents that need a pending credential fail with credential not configured until the user fills it in; the PENDING sentinel is never injected into the agent’s environment, so the LLM can’t read or exfiltrate it. After apply, the CLI prints the list of pending env vars so you know what’s still left to fill in:

Skills: three sources

Every skill entry picks one source:
Agents reference skills by slug:
The validator rejects dangling references (agent points at a skill the manifest doesn’t declare) — no half-applied state.

Sidecar services (Redis, Postgres, etc.)

Declare sidecar containers that run alongside the agent on the crew bridge network. Agents reach them by service name — redis:6379, postgres:5432 — over a private network that’s never published to the host.
No credentials: block, and no env_refs for the passwords: both images are in the auto-managed catalog, so apply generates POSTGRES_PASSWORD and REDIS_PASSWORD, boots each sidecar with its own secret and adds both to every agent in the crew. Declaring one of those names in credentials: as well is rejected at validate time — the manual row and the generated one would race for the same workspace credential (#1712).
What the provisioner does for each service:
  1. Pull the image (best-effort: tolerates registry outages when a local copy exists).
  2. Create per-crew named volumes (crewship-svc-{crew-slug}-{crew-id}-vol-{name}).
  3. Start the container with a DNS alias matching name, on the crew bridge.
  4. Wait for healthcheck.test to report HEALTHY — capped at 60 s across all sidecars in the crew (internal/provider/docker/sidecar.go:EnsureCrewServices).
  5. Only then start the agent runtime.
A failed healthcheck (timeout, non-zero exit) prevents the agent from starting — the error surfaces as sidecar "X" not healthy: .... This is intentional: silently proceeding would mask half-broken setups that look fine until the first DB query times out. Test your healthcheck command locally before committing the manifest.
Services declared without a healthcheck: block aren’t gated — Crewship considers them ready as soon as Docker reports the container running. Use a healthcheck whenever the agent’s first call depends on the service being initialized (Postgres after initdb, Redis after AOF replay, etc.).
Volumes are always named volumes. The validator rejects bind-mount paths so manifests stay portable across machines.

Auto-managed service credentials

Rather than hand-declaring a GENERIC_SECRET credential and wiring env_refs, a service asks Crewship to generate and manage the secret itself — which is what the crew above relies on:
Each entry produces an AUTO_MANAGED credential row at apply time (attributed to the crew’s lead agent), is injected into the sidecar (as an env var by default, or as a command arg via inject_as_command for images like Redis), and is appended to every crew agent’s env_refs automatically so agents can reach the service with the right value. Fields and defaults: You rarely write this by hand: for well-known images (postgres:*, mysql:*, mariadb:*, mongo:*, redis:*, rabbitmq:*, …) the parser merges a sugar default in automatically — see auto-managed credentials for the full image catalog. Explicit entries win over sugar defaults with the same name.
Datastores are always password-protected, Redis included. A stock redis:* sidecar boots with --requirepass <generated> and hands agents the secret as a REDIS_PASSWORD env credential — bridge isolation is never the only gate. Supply your own command: on the service to opt out and manage the password yourself.
A service that publishes a port to the host cannot use auto_credentials — the validator refuses the combination. Once the sidecar leaves the crew bridge, the external attack surface deserves an operator-chosen credential.

When sidecars start — and when they are removed

Sidecars come up with the crew’s container, whatever woke it: a chat message, crewship issue start, a scheduled agent, an inbound webhook, a routine’s agent or script step, the web terminal, or the dashboard’s start button. All of those go through the same crew-start contract (internal/crewstart), so a crew that declares services: gets them on every path.
Before v1.x this was true of the chat path only — a crew whose first activity was issue start, a schedule or a webhook came up with no sidecars at all and no error anywhere, so redis:6379 simply did not resolve inside the container (#1708). If you drove Crewship headlessly and concluded services: did not work, this is why. Verify with crewship crew services <crew>: it reads the live container runtime, not your manifest.
Starting them is idempotent — a sidecar that is already up is reattached to, not recreated — as long as its spec has not changed. The spec includes the service’s environment, which is why env_refs resolve against the crew’s credentials (the credential_crews link a workspace secret gets when it is attached to a crew) rather than against whichever agent happened to trigger the start: a per-agent value would give the same crew a different Postgres per agent and restart it whenever the trigger changed. On a container provider without sidecar support (Apple Containers today) the crew still starts. The drop is logged, and chat shows “Sidecar services declared but this container provider doesn’t support them yet” rather than letting the crew believe it has a database. A sidecar that fails to start (image pull, port clash, failing healthcheck) fails the crew start everywhere except the web terminal, which opens the shell anyway and says so — that is the surface you use to diagnose the failure, and the crew’s own container is up by then.
crewship crew delete now destroys the crew’s sidecars and their named volumes. The containers are force-removed and crewship-svc-{crew}-{crew-id}-vol-* — including a Postgres data directory — is deleted with them. Previously both were left running forever (#1709), which leaked disk and a live, still-authenticated database per deleted crew; the flip side is that deleting a crew is now the same decision as dropping its datastores. Back up first (crewship backup create) if the data matters. The crew’s own runtime container is not removed — it has an idle TTL and a reaper; sidecars have neither, which is why they are handled here.
The confirmation prompt says this too, and names the volumes, so it does not depend on having read this page:
A crew with no services: gets the plain one-line prompt it always had, and --yes skips the confirmation as it always did — so a script that already passes --yes deletes volumes without asking. Audit those before upgrading.
Two crews with the same slug are separate all the way down. crews is unique on (workspace, slug), not on slug alone, so two workspaces can both hold a data-crew — and one crewshipd serves every workspace against one Docker daemon. Sidecar containers and volumes therefore carry the globally-unique crew id, the way the crew’s own container has since audit C1:Start, stop, list and delete all select on that label by exact equality — never by a name prefix, because slugs may contain hyphens and one crew’s prefix can prefix another’s names (data vs data-vol-x). Deleting one workspace’s data-crew leaves the other workspace’s data-crew Postgres running with its data intact, and each crew’s own cleanup still happens (#1732; before it, the delete refused outright whenever a slug was shared).
Upgrading across #1732 re-keys sidecars on the crew’s next start. Instances that ran an earlier build have sidecars under the old slug-only names. The first EnsureCrewServices after upgrade migrates them, per crew, per service:
  • the legacy container is stopped and removed — it is ephemeral compute, and the id-scoped replacement starts moments later in the same call. Leaving it would put two containers on the crew bridge answering to the same service alias, and the agent would round-robin between its real database and a freshly-initialised empty one;
  • the legacy volume’s data is copied into the id-scoped volume by a short-lived helper running the sidecar’s own image, and the legacy volume is pruned only after that copy exits 0.
If the copy cannot complete, the legacy volume is left untouched and the crew’s services do not start, with an error naming both volumes — a paused start is recoverable, an emptied database is not. If the id-scoped volume already exists, the legacy one is left in place and logged rather than clobbering it.Two crews that shared a slug before the upgrade see the same legacy volume, and nothing on the daemon can say which owned it: the first to start claims the data, the other gets a clean volume, and the log says so. Back up first (crewship backup create) if that ambiguity applies to you. crewship crew delete will not sweep pre-#1732 sidecars for the same reason — start the crew once to migrate them, or remove them by hand.

Inventory: what’s actually running

The manifest above is what you declared. What’s actually running can drift from it — a sidecar you stopped by hand while debugging, or one Docker OOM-killed, still shows up as “configured” if you only look at the crew’s stored config. crewship crew services answers the live question instead, reading straight from Docker rather than any stored snapshot:
  • STATUS is the container’s current state (running / stopped / creating / error) — the same vocabulary crewship crew container-status uses for the crew’s own runtime container.
  • TYPE is inferred from the image (postgres / redis / mysql / mongodb / other), the same inference the capabilities endpoint uses for a crew’s datastore capabilities.
  • PORTS are the container-internal ports the sidecar exposes — never a host mapping, since sidecars are never published to the host (see above).
This is a read: it never starts, stops, or recreates anything. A service your manifest declares but whose container isn’t running yet (first apply still in flight, or it failed to start) simply doesn’t appear until Docker reports it. The CLI wraps GET /api/v1/crews/{crewId}/services, which any agent can call directly for the same live facts as JSON.

Delivering files into the crew: files:

A crew spec can carry local files (scripts, fixtures, configs) that apply delivers into the crew’s shared volume — visible in-container under /crew/shared. This is how a routine’s deterministic scripts travel with the workspace manifest instead of a manual crew files save after every rebuild:
Rules the planner enforces:
  • dest must stay under shared/ (/crew/shared/... spellings are normalized). The crew’s /output, /secrets, and agent homes are off-limits to declarative delivery on purpose; traversal is rejected.
  • Each file is capped at 1 MiB — the same budget as an inline code-step body. Bigger assets belong in object storage or the devcontainer image.
  • On a running crew, an overwrite routes through the container (PUT /api/v1/crews/{crewId}/files/save) so in-container permissions stay correct. Re-delivering byte-identical content is a no-op and succeeds even on a stopped crew; delivering changed content to a stopped crew returns 409 — start the crew and re-apply.

Alternative: in-container databases via devcontainer features

If you prefer the database’s lifecycle to match the agent’s container (a smaller setup, but DB restarts whenever the agent restarts), you can use devcontainer features instead:
The agent reaches both at localhost:5432 and localhost:6379. Works today with zero provisioner changes, but the DB lives inside the same container as the agent.

Workspace bundles: multi-crew setups

When you want to ship more than one crew in a single file, switch to kind: Workspace:
Workspace-scoped credentials and skills are available to every nested crew. A nested crew can override or add to them in its own credentials: / skills: block.

Plan → confirm → apply

Apply is two-pass: it computes the full plan first, prints it, asks for confirmation on anything destructive, and only then mutates. The flow mirrors terraform plan && terraform apply so you can always see what’s about to change.
--dry-run runs the plan phase only — perfect for code review or CI:

Exporting an existing workspace

crewship export is the round-trip partner of apply. It pulls the current workspace state and renders it as a manifest — useful for backing up, snapshotting, or migrating a workspace someone else built in the UI.
The export includes credential slots (without values — they never travel in the file) and skill bodies. Re-applying the exported file on a fresh workspace recreates the same shape:

IDE autocomplete

The hosted JSON Schema endpoint (https://schemas.crewship.ai/v1/manifest.json) is not live yet — it ships in a follow-up release. Until then, validation falls back to the YAML language server’s basic shape checks. The crewship apply --dry-run path is the authoritative validator in the meantime.
Once published, add this line at the top of any manifest and your editor (VS Code, Cursor, JetBrains with the YAML language server) will get autocomplete and inline validation:

CI patterns

Pair with secret managers via --secrets-file:

Examples

The repo ships with these reference manifests under examples/manifests/:

See also