kind: TriageRule
What it is
kind: TriageRule declares a workspace-scoped automation that
inspects incoming issues — their title, body, originating agent, or
originating crew — and applies a fixed set of mutations
(add_labels, set priority, assign to a project, route to an
agent, change status) whenever the conditions match. Rules form an
ordered pipeline: each issue runs through every enabled rule, low
priority numbers first, and the first match wins. They are the
declarative form of “if a bug report mentions ‘crash’, label it
bug and assign it to the on-call agent.”
Under the hood the server stores the structured match/action blocks
as opaque JSON TEXT columns (match_json, actions_json), but the
manifest exposes them as nested objects so YAML stays readable. The
apply pipeline marshals them on the way in, and crewship export
unmarshals them on the way out.
YAML schema
Examples
Auto-label bug reports
The canonical use-case: any issue whose title mentions a crash- or error-related keyword gets taggedbug and bumped to high priority.
Label named bug existing in the same
workspace. If the manifest declares both, the topological apply
order in internal/manifest/apply.go (Phase 4: Labels runs before
Phase 15: TriageRules) guarantees the label exists by the time
the triage rule’s POST body is built.
Auto-assign Discord pulls to the trapper agent
A more elaborate rule: any issue raised by thediscord-puller
agent inside the uo-outlands crew is automatically routed to the
q2-roadmap project, assigned to the trapper agent, and tagged
discord + automation.
priority: 50 is lower than the bug-auto-label rule’s
priority: 100, this Discord-specific rule fires first. If it
doesn’t match (issue isn’t from the discord-puller), evaluation
continues to the next rule.
Cross-kind FK references
A complete deployable bundle: Project + Labels + the rule that references both.CLI reference
The existingcrewship triage surface is intentionally minimal —
list + process only. Per-rule CRUD is manifest-driven: there is no
crewship triage create / get / delete subcommand today, and the only
declarative path is crewship apply. The manifest pipeline uses the
underlying CRUD REST endpoints (see the mapping table below) under the
hood.
| Command | Description |
|---|---|
crewship triage list | List every triage rule in the workspace. |
crewship triage process | Evaluate all enabled rules against the backlog (one-shot manual fire). |
crewship apply --file triage.yaml | Create / update rules from a manifest. |
crewship apply --file triage.yaml --replace --yes | Destructive recreate (delete-then-create per rule). |
crewship export workspace | Round-trip — emits one TriageRule doc per stored row. |
REST endpoint mapping
The manifest’s structuredmatch / actions blocks marshal into
two JSON TEXT columns on the server. The mapping is one-way at
apply time and reversed at export time.
| Manifest field | POST body field | DB column |
|---|---|---|
metadata.name | name | triage_rules.name |
metadata.slug | slug | (advisory only — no slug column) |
spec.enabled | enabled | triage_rules.enabled |
spec.priority (default 100) | priority | triage_rules.priority |
spec.match.* (all nested fields) | match_json (string) | triage_rules.match_json |
spec.actions.* (all nested fields) | actions_json (string) | triage_rules.actions_json |
match_json and actions_json are JSON-encoded strings whose
keys are the snake_case versions of the YAML field names
(title_contains, from_agent_slug, add_labels,
assign_to_project_slug, etc.) — the exact JSON tags on the
TriageMatch and TriageActions Go structs.
| Endpoint | Verb | Used by |
|---|---|---|
/api/v1/triage-rules | GET | Plan (lookup), Export |
/api/v1/triage-rules | POST | Plan (create) |
/api/v1/triage-rules/{id} | PATCH | Plan (update) |
/api/v1/triage-rules/{id} | DELETE | ApplyReplace |
Validation rules
TriageRuleDocument.Validate(ctx) enforces every rule in one pass
and returns all violations joined into a single error so the user
gets the full picture per apply attempt.
metadata.nameandmetadata.slugare required (both must be non-empty after trim).spec.matchmust have at least one non-empty field (title_contains,body_contains,from_agent_slug, orfrom_crew_slug). A wholly empty match would fire on every issue — almost always a manifest authoring mistake.- Every slug in
spec.actions.add_labelsmust resolve viactx.HasLabel(slug)— i.e. it appears in the manifest’sDeclaredLabelsor in the workspace’sRemoteLabels. spec.actions.assign_to_project_slug, if set, must resolve viactx.HasProject(slug).spec.actions.assign_to_agent_slug, if set, must resolve viactx.HasAgent(slug).spec.match.from_agent_slug, if set, must resolve viactx.HasAgent(slug).spec.match.from_crew_slug, if set, must resolve viactx.HasCrew(slug).spec.actions.add_labelsentries that are empty strings are rejected (use[]or omit the field instead).
spec.priority is an int. Zero or absent values are treated as
the default (100) at apply time — they’re not a validation error.
Apply behavior
Default mode (Upsert)
For each declared TriageRule:
- Fetch every existing rule via
GET /api/v1/triage-rules. - Match by
metadata.nameagainst the server’snamefield (the natural key — the server has noslugcolumn). - If no match: emit
Action=Create→ POST the rule with the structured match/actions blocks marshaled intomatch_json/actions_jsonstrings. - If match found but any of differ: emit
Action=Update→ PATCH the rule. - If match found and every field is byte-identical (after
re-marshaling both sides through the same JSON encoder):
emit
Action=Unchanged.
ApplyStrict
Strict mode is enforced at the parent apply.go level, not inside
TriageRule.Plan: if any declared rule’s name collides with an
existing server-side rule, the parent aborts before invoking Plan.
The TriageRule kind itself has no extra strict behavior.
ApplyReplace
The parent layer emits an Action=Delete for every server-side
rule whose name is in the manifest, then re-runs Plan with
remote=nil so each kind issues a fresh Action=Create. The
TriageRule kind doesn’t need a special-case branch for replace —
its existing nil-remote path covers the recreation step.
Round-trip via export
crewship export workspace invokes ExportTriageRules, which:
- Fetches every rule via
GET /api/v1/triage-rules. - For each row, unmarshals
match_jsonandactions_jsonback into structuredTriageMatch/TriageActionsGo values so the YAML output is readable, not JSON-encoded strings. - Derives a
metadata.slugfrom the row’snamefield (kebab-case) — the server doesn’t store a slug column for triage rules, so we deterministically slugify on export. The operator can override by editing the exported YAML before re-apply. - Tolerates corrupt
match_json/actions_jsonserver-side: the export emits the rest of the document with the corrupted field at its zero value rather than failing outright. (A reapply of the exported YAML will re-write the JSON column.)
apply → export → apply
produces zero plan items the second time.
See also
- Label —
actions.add_labelsreferences Label slugs - Project —
actions.assign_to_project_slugreferences Project slugs - RecurringIssue — sibling kind for time-based (rather than match-based) issue creation
- SavedView — uses the same label/project FK conventions for filter expressions
internal/api/triage_handler.go— backend handler that serves the REST endpoints this kind targets