Skip to main content

kind: Crew

What it is

kind: Crew is the full-CRUD authoring surface for one crew: Create on first apply, Update on drift, Unchanged when the declared spec already matches the server. Where CrewTemplate is a one-shot deploy of a server-side blueprint (no in-place updates), kind: Crew owns every field of the crew row directly — runtime image, devcontainer overlay, mise toolchain, and sidecar services. Both kinds write to the same crews table; the difference is provenance and lifecycle. An exported workspace round-trips through kind: Crew for crews whose slug doesn’t match a known template (the common case, since operators usually rename on deploy). The kind is implemented in internal/manifest/kinds/crew.go.
Agents are separate documents. A standalone kind: Crew document defines only the crew row (and its sidecars / container config). Its agents are authored either as standalone kind: Agent documents that reference this crew by crew_slug, or inline under a kind: Workspace bundle’s nested crew shape. The standalone Crew document does not carry an agents: list.

YAML schema

Top-level spec fields

spec.devcontainer

Models the subset of devcontainer.json operators commonly tweak; unmodeled keys pass through via raw: (typed fields win on collision).

Pinning features

A ref may name a tag or a digest, and the difference decides whether your crew is reproducible.
A tag keeps moving upstream. Crewship’s build cache is keyed on the ref as you wrote it, so when :2 starts pointing at a newer 2.x the key is unchanged: the crew keeps the image it already has, and nothing announces that a newer one exists. That is the right default for stability — a crew does not silently change under you — but it means the tag alone does not tell you what you are running. Every build therefore records what each ref resolved to. Read it back with:
= is pinned, ~ is floating. The same information appears on the feature chips in the Builder popover, where a floating feature is tinted and its tooltip carries the resolved digest. Two caveats worth knowing:
  • A feature’s own options can float independently. VERSION: latest installs the newest tool on every rebuild even when the feature ref is pinned to a digest — pin the option too if you need the tool version fixed.
  • Crews provisioned before version tracking existed report (not recorded — provisioned before version tracking). Rebuild to record it.
spec.runtime_image is the canonical image. The legacy spec.devcontainer.image is the same thing — set one only; a divergent pair is rejected as a paste mistake.

spec.mise

spec.services[]

Each entry is one sidecar on the crew’s private bridge network. The wire shape mirrors the server’s serviceWire.

spec.files[]

Local files (scripts, fixtures, configs) delivered into the crew’s shared volume at crewship apply time — visible in-container under /crew/shared. This is how a routine’s deterministic scripts travel with the manifest instead of a manual crewship crew files save after every rebuild. Re-applied on every apply (idempotent PUT through the same /files/save endpoint).
A type: script routine step (see docs/manifest/routine.md “Script steps”) can then run them token-zero: path: scripts/parse_vypis.py. A missing or oversized local file fails the plan (and --dry-run), not mid-apply. Bundled files are written to the crew’s /crew/shared bind source on the host, so they are present the moment the crew container comes up — including crews that have no agents and whose container is provisioned lazily on the first routine run. Inspect them with crewship crew files list <crew> --path shared.

Examples

Minimal

With devcontainer sizing + mise toolchain

With a Postgres sidecar

CLI reference

REST endpoint mapping

How each manifest field maps onto the create/update request body: Endpoints used:

Validation rules

CrewDocument.Validate (offline) enforces:
  • apiVersion / kind, when set, equal crewship/v1 / Crew.
  • metadata.name and metadata.slug are non-empty; slug is kebab-case.
  • spec.color, when it starts with #, is a valid 6-digit hex code.
  • spec.runtime_image is required (no sane default; the server tolerates NULL but falls back to a wrong-for-you built-in image).
  • spec.devcontainer.image, when set, must equal spec.runtime_image.
  • devcontainer.memory_mb is 6–262144 and cpus is 0.01–512 (0 or an omitted key means “use the server default” — 4096 MB and 2.0 CPUs). These lower bounds are Docker’s own: a crew configured below them is rejected here, at apply time, rather than passing every layer and then failing inside the daemon on every wake — which wedges every run of that crew. The upper bound on cpus cannot be fully checked server-side, because the daemon’s real limit is the host’s core count. There is a second, higher floor — the memory one agent actually needs, 2048 MB by default. A crew between the two applies successfully and the server returns a warning: the container is created and the agent CLI is then OOM-killed (exit 137) on start. Validate does not enforce it, deliberately. That floor is the runtime.agent_min_memory_mb / runtime.agent_min_cpus instance settings, which an operator can move, and offline validation (apply --dry-run with no server) cannot read them — a compiled-in copy would reject manifests the server would accept. See crewship crew for the settings and their defaults.
  • Each service: DNS-label name, unique within the crew, image present, numeric ports only, parseable healthcheck durations, named (not bind) volumes with unique mounts.
The assembled devcontainer / mise JSON is not round-tripped through the server’s parser at Validate time (Validate runs offline); the server re-validates on every write, so any deeper schema error surfaces at Apply.

Apply behavior

ApplyUpsert (default)

  • Remote missing → ActionCreate: POST the full body.
  • Remote present, fields drift → ActionUpdate: a sparse PATCH of only the drifted fields. Empty declared scalars and nil devcontainer/mise/services are skipped — so the manifest never overwrites a value the operator set via the UI.
  • Remote present, no drift → ActionUnchanged.
devcontainer_config / mise_config / services_json are compared after JSON normalisation, so server-side key reordering doesn’t trigger phantom drift. services: [] (empty array) clears all sidecars; an absent services: key leaves them alone.

Round-trip via export

crewship export workspace calls ExportCrews, which renders one kind: Crew per crew (sorted by slug). The devcontainer / mise JSON is decoded back into the typed sub-fields where possible, with anything unmodeled stashed under raw: so the round-trip stays byte-stable. Columns the manifest doesn’t model (cached_image, config_hash, container_ttl_hours, network_mode) are dropped.

See also

  • Agent — references this crew via spec.crew_slug.
  • CrewTemplate — one-shot deploy of a server blueprint.
  • Integration — crew-scoped MCP servers.
  • Workspace — the top-level bundle that nests crews + agents.
  • Backend: internal/api/crews_create.go, internal/api/crews_update.go, internal/api/crew_services.go.
  • This kind’s Go implementation: internal/manifest/kinds/crew.go.