kind: CrewTemplate
What it is
ACrewTemplate is a deploy-only reference kind. The manifest entry instantiates an existing template (creating a new crew + its agents from the template’s blueprint) but cannot author the template itself. Template authoring belongs to the seeded built-in catalog under internal/database or to direct workspace inserts into the crew_templates table — both outside the manifest pipeline.
In other words: a CrewTemplate document is the YAML equivalent of clicking “Deploy” on a card in the templates UI.
One-shot semantics — there is no update flow
The backend endpoint behind this kind is:errCrewSlugConflict). There is no PATCH /deploy, no DELETE /deploy, and no diff-and-reapply path. Once a crew exists under the crew_slug_override slug, this kind has nothing left to do.
As a consequence the planner emits only two actions:
| Situation | Plan action | What happens |
|---|---|---|
Source template exists, override slug free, deploy: true | Create | POST /deploy with the body below |
| Override slug already names a crew | Unchanged | No HTTP traffic |
deploy: false (any state) | Unchanged | No HTTP traffic, warning in description |
| Source template missing | Plan returns error | Apply aborts before any mutation |
Idempotency is by slug alone. Thecrewstable has notemplate_slugprovenance column, so a pre-existing crew whose slug happens to equalcrew_slug_overrideis treated as “already deployed” — even if it was actually created by hand viakind: Crew. By design: the server would 409 on the deploy POST anyway, and the manifest’s job is to converge to “a crew with this slug exists”, not to enforce who created it.
deploy: falseis inert. Settingdeploy: falsedoes not trigger an “undeploy”. There is no inverse endpoint. To remove a crew that came from a template, delete it viakind: Crew(orcrewship crew delete <slug>). The CrewTemplate planner records intent and surfaces a warning in dry-run output so the no-op is visible.
YAML schema
Field reference
| Field | Required | Type | Notes |
|---|---|---|---|
apiVersion | yes | string | Always crewship/v1. |
kind | yes | string | Always CrewTemplate. |
metadata.name | yes | string | Human label. Sent to the deploy handler as crew_name — the server 400s on empty. |
metadata.slug | yes | string | Slug of the SOURCE template. Must match a row in GET /api/v1/crew-templates. Existence is checked at Plan time, not Validate. |
metadata.description | no | string | Informational; not forwarded. |
spec.deploy | no | bool | Defaults to false in Go (bool zero value). Author YAML should set it explicitly. true = deploy if missing, false = no-op. |
spec.crew_slug_override | yes | string (kebab-case) | The slug the new crew will carry. One template can deploy many crews, so this is what distinguishes them. Validated as ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$ at Validate time; uniqueness is enforced server-side at Apply. |
spec.inputs | no | map[string]any | Forwarded verbatim to the deploy endpoint. The current server ignores unknown keys via readJSON’s tolerant decoding, so future template parameters can land without a manifest schema bump. |
Examples
Minimal example — deploy a built-in template
Deploy the same template twice under different override slugs
A single template can back many crews —crew_slug_override distinguishes them:
eng-team-a and eng-team-b exist. Re-applying the same bundle is a pure Unchanged plan (no HTTP traffic to the deploy endpoint).
Deploy with inputs (forward-compatible)
inputs map is currently ignored server-side; future versions of the deploy handler may consume specific keys. Manifest authors can include them today without breaking compatibility.
deploy: false — pin without re-applying
Unchanged with a warning. Useful for keeping a CrewTemplate document in a versioned bundle for documentation purposes while temporarily disabling automatic deploys.
CLI reference
The crew-template surface has a dedicated CLI undercrewship template … (no hyphen — registered as templateCmd in cmd/crewship/cmd_template.go); manifest-level apply does not add new subcommands beyond the global apply / export.
| Command | One-liner |
|---|---|
crewship template list | List the workspace’s templates (built-in + workspace-authored). |
crewship template get <slug> | Show a single template’s agent list. |
crewship template deploy <slug> --name <name> | Imperative deploy (equivalent to applying one CrewTemplate doc). |
crewship apply --file <file> | Declarative apply — deploys every CrewTemplate doc whose override slug isn’t yet realised. |
crewship export workspace | Emits CrewTemplate docs for crews whose slug heuristically matches a template (see “Round-trip via export”). |
REST endpoint mapping
| Manifest field | HTTP verb + path | Request body |
|---|---|---|
spec.deploy: true, override slug free | POST /api/v1/crew-templates/{metadata.slug}/deploy | {crew_name, crew_slug, inputs?} |
spec.deploy: true, override slug taken | — | No request issued; plan is Unchanged |
spec.deploy: false (any state) | — | No request issued; plan is Unchanged (with warning) |
| Source template existence check | GET /api/v1/crew-templates | — |
| Override slug existence check | GET /api/v1/crews | — |
| Export | GET /api/v1/crew-templates + GET /api/v1/crews | Heuristic match (see below) |
POST body shape
The deploy handler (internal/api/crew_templates.go) reads two fields and tolerates extras:
crew_name→metadata.name. Server 400s on empty.crew_slug→spec.crew_slug_override. Server slugifies + validates; collision returns 409.inputs→ forwarded but currently unused. Forward compatibility hook.
DB columns touched (informational)
The deploy handler writes one row tocrews (id, workspace_id, name, slug, icon, color, timestamps) and N rows to agents (one per template agent, with agent_slug = template_agent.slug + "-" + crew_slug). It does not record provenance — there is no crews.template_slug column.
Validation rules
Validate (offline, no HTTP) enforces:
apiVersionmust be empty orcrewship/v1.kindmust be empty orCrewTemplate.metadata.nameis non-empty (sent ascrew_name).metadata.slugis non-empty (source template slug; existence is checked at Plan time).spec.crew_slug_overrideis non-empty.spec.crew_slug_overridematches^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$(kebab-case, no leading/trailing hyphen, no underscores, no uppercase).
metadata.slug actually exists in GET /api/v1/crew-templates) is not checked at validate time — WorkspaceContext does not carry remote templates. The check is deferred to Plan, where the live HTTP client is available, and surfaces as a transparent “source template not found” error in dry-run output.
Apply behavior
Default (ApplyUpsert)
Plan decision table:
spec.deploy | Override slug occupied? | Source template present? | Action | Notes |
|---|---|---|---|---|
true | no | yes | Create | POST /deploy |
true | yes | yes | Unchanged | No HTTP traffic |
true | — | no | Plan error | Apply aborts before any mutation |
false | no | yes | Unchanged | Warning: “deploy=false — no action” |
false | yes | yes | Unchanged | Warning: “cannot undeploy” |
ApplyStrict
Strict mode adds no extra constraints for this kind. A crew already occupying the override slug is not a strict-mode error — it is the expected Unchanged outcome. Strict mode only fires for kinds whose backend supports re-creation; deploy is one-shot, so strict semantics collapse onto upsert semantics.
ApplyReplace
Replace mode is also a no-op for the deploy itself: there is no DELETE endpoint at /api/v1/crew-templates/{slug}/deploy, so even “destructively recreate” cannot undeploy the existing crew. To remove a crew before re-deploying, declare a kind: Crew document with the same slug and delete it via that kind’s Replace semantics, or use crewship crew delete <slug> first. When the backend grows an inverse deploy verb this kind will start emitting Delete + Create pairs in Replace mode without any YAML schema change.
Round-trip via export
Provenance gap
Thecrews schema has no template_slug column today. The deploy handler writes the crew row without recording the source template. There is therefore no authoritative way to round-trip a deployed crew back into a kind: CrewTemplate document.
ExportCrewTemplates makes a best-effort heuristic match: a crew whose slug exactly equals a known template’s slug is reported as a CrewTemplate deployment with crew_slug_override = crew.slug. Crews whose slugs diverge from any template slug — the common case, since operators usually rename — are not exported as CrewTemplate; they round-trip via kind: Crew instead.
| Crew slug | Matching template? | Exported as |
|---|---|---|
engineering-team | yes (built-in) | kind: CrewTemplate |
my-custom-crew | no | kind: Crew (other agent) |
eng-team-a | no | kind: Crew (other agent) |
Crew documents, not template deployments — losing the “deployed from X” semantic but preserving the actual state of the world.
When/if the server grows a template_slug column on crews, the heuristic in ExportCrewTemplates switches to an exact provenance check; the public Export* signature stays the same.
Emitted document shape
For each match, the export produces:CrewTemplate doc is a pure Unchanged plan (the crew is already there).
See also
docs/manifest/recipe.md— sibling install-only reference kind.docs/manifest/connector.md— sibling install-only reference kind (integrations).internal/api/crew_templates.go— backend handler forGET /api/v1/crew-templates,GET /api/v1/crew-templates/{slug},POST /api/v1/crew-templates/{slug}/deploy.internal/database/seed_crew_templates.go— source of truth for built-in templates that ship with the binary.internal/manifest/kinds/crew_template.go— this kind’s Go implementation.