kind: WorkflowTemplate
What it is
AWorkflowTemplate defines the state machine an issue, run, or other tracked item moves through inside a workspace. It’s the equivalent of “status options on a board”: you declare an ordered list of stages, each tagged with a type (open, started, completed, or cancelled), and the workspace UI uses that as the column layout for kanban boards plus the legal transition graph for status changes.
WorkflowTemplates are workspace-scoped and idempotent on metadata.slug within a workspace. The workflow_templates DB table has no slug column, so on export the manifest synthesises a kebab-case slug from metadata.name to keep round-trips stable.
Built-in templates seeded by the server (sequential, parallel, dev-test-loop, pipeline) are owned by the server and explicitly excluded from export — re-applying an exported workspace will never overwrite them and never produce drift against them.
YAML schema
Field reference
| Field | Required | Type | Notes |
|---|---|---|---|
apiVersion | yes | string | Always crewship/v1. |
kind | yes | string | Always WorkflowTemplate. |
metadata.name | yes | string | Stored verbatim. Drives the UI label and is the server-side uniqueness key (per workspace). |
metadata.slug | yes | string | Workspace-unique manifest identifier. |
spec.description | no | string | Free-form text shown on the template detail panel. |
spec.icon | no | string | Emoji shortcode (:hammer_and_wrench:) or icon slug — the UI looks it up against its icon set. |
spec.color | no | string | #RRGGBB. Three-digit shorthand is rejected. |
spec.stages | yes | array | Ordered list of stage objects. Must be non-empty. |
spec.stages[].name | yes | string | Unique within the template. Lowercase + underscores conventionally; the UI displays them with that casing. |
spec.stages[].type | yes | enum | One of open, started, completed, cancelled — see Stage types below. |
spec.stages[].position | yes | int | Unique within the template. Drives the UI column order. |
spec.stages[].color | no | string | #RRGGBB. Same constraint as spec.color. |
Stage types
Every stage carries one of fourtype tags. The tag is what the orchestrator and UI key on for behaviour — not the stage name, which is purely human-facing.
| Type | Semantics | Cardinality |
|---|---|---|
open | Entry state. Newly-created items land here. Exactly one stage per template must be open; that stage is the implicit default when an item is created without a status. | Exactly 1 |
started | In-progress states. The item is being worked on but is not yet terminal. Multiple started stages are common (e.g. in_progress, in_review, blocked). | 0..n |
completed | Terminal success states. The item is done and counts toward “completed” in burn-down / velocity rollups. At least one is required so an item can ever finish. | 1..n |
cancelled | Terminal failure / abandon states. The item is done but is excluded from completion metrics. Use this for “won’t fix”, “duplicate”, “out of scope” — anything that closes the item without counting as progress. | 0..n |
Examples
Minimal example
open stage and one completed stage. No started and no cancelled are fine.
Realistic example with all common fields
ready row above intentionally illustrates a rejected shape — Validate refuses two open stages. Pick one of backlog/ready as the entry state and tag the other started.
Multi-template bundle
CLI reference
crewship apply path is the only one that resolves metadata.slug → server ID for you. The flat crewship workflow get/delete accept a slug for convenience and do the lookup themselves.
REST endpoint mapping
| Manifest field | POST body field | DB column |
|---|---|---|
metadata.name | name | workflow_templates.name |
spec.description | description | workflow_templates.description |
spec.stages | template_json (serialised) | workflow_templates.template_json (TEXT) |
spec.icon | icon | workflow_templates.icon |
spec.color | color | workflow_templates.color |
(server-set, always false for user-created) | — | workflow_templates.is_builtin |
| (server-assigned) | — | workflow_templates.id, workspace_id, created_at, updated_at |
template_json column stores the entire stages array as a JSON string (the column is TEXT, not JSON). The handler does not re-parse user input — it passes the marshalled string straight through to the DB, so any future stage-shape extension is forward-compatible.
| Operation | Method | Path |
|---|---|---|
| List | GET | /api/v1/workflow-templates |
| Get | GET | /api/v1/workflow-templates/{id} |
| Create | POST | /api/v1/workflow-templates |
| Update | PATCH | /api/v1/workflow-templates/{id} |
| Delete | DELETE | /api/v1/workflow-templates/{id} |
Validation rules
metadata.nameis required (server rejects emptynamewith HTTP 400).metadata.slugis required and must be unique within the workspace.spec.stagesmust be a non-empty array.- Each
spec.stages[].namemust be non-empty and unique within the template. - Each
spec.stages[].positionmust be unique within the template. - Each
spec.stages[].typemust be one ofopen,started,completed,cancelled. - Exactly one stage must have
type=open. - At least one stage must have
type=completed. spec.colorandspec.stages[].color, if set, must match^#[0-9A-Fa-f]{6}$. Three-digit shorthand is not accepted.
Apply behavior
ApplyUpsert (default)
- List the workspace’s templates via
GET /api/v1/workflow-templates. - Match by
metadata.name:- No match →
Action=Create,POST /api/v1/workflow-templateswith{name, description, template_json, icon, color}. - Match with differing fields →
Action=Update,PATCH /api/v1/workflow-templates/{id}carrying the same body. - Match with identical fields →
Action=Unchanged, no HTTP call issued.
- No match →
position, so reordering the stages array in the YAML file without changing positions is a no-op (Unchanged), not a drift.
ApplyStrict
Fails with a slug already exists error if any declared template already exists by name. Useful for new-workspace bootstrapping where overwriting a same-named template would be a bug.
ApplyReplace
For each declared template, emits Action=Delete followed by Action=Create. Use this only when you want a fresh row (e.g. you’ve renamed a stage and want the underlying record reset rather than mutated in place). Apply also deletes any templates in the workspace that the manifest no longer declares — but never the built-in templates, which are protected server-side.
Round-trip via export
crewship export workspace lists every non-builtin template and emits one kind: WorkflowTemplate document per row. The export decodes template_json back into the structured stages array so the output is directly re-applyable:
Engineering Standard → engineering-standard). If two templates in the same workspace share a name (rejected by the DB’s UNIQUE(workspace_id, name) index — so this should never happen in practice), their slugs would collide and crewship apply would reject the bundle.
Built-in templates (sequential, parallel, dev-test-loop, pipeline, plus any others the server seeds in the future) are filtered out. The intent is that re-applying an exported workspace bundle never produces drift against server-owned rows.
See also
- kind: Project — projects don’t reference workflow templates directly today, but a future
default_workflow_slugfield is planned. - kind: TriageRule —
actions.set_statusreferences a stage name; if you wire one up, make sure the stage exists in whichever template the matched issue lives under. - kind: SavedView — views can filter by stage type (
type=open/type=started) to render “active work” boards. - SPEC-2 section 8 — authoritative contract for this kind.