Skip to main content

Triage Rules

A triage rule matches an issue title against a pattern (substring, regex, or exact) and applies a bundle of updates: pick a crew, set an assignee, set priority, project, and labels. Rules are evaluated in position order; the first matching rule wins per issue (other rules don’t apply, even if they’d also match). Implementation: internal/api/triage_handler.go. Backed by the triage_rules table.
All endpoints require an authenticated session and workspace context.

Endpoints

Rule shape

Rule CRUD

Manage the ordered rule set. Rules evaluate in position order; first match wins.

GET /api/v1/triage-rules

List every rule in the workspace, ordered by position ASC, created_at ASC. Request: No request body or query parameters. Auth: authenticated session + workspace context. Response: 200 OK — JSON array (never null).

POST /api/v1/triage-rules

Create a new rule. Auto-positioned at the end of the queue (MAX(position) + 1); reorder with PATCH. Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role (requireRole("create")). Request body: Rules are created enabled = true and match_count = 0. Response: 201 Created with the rule object (same shape as List). WebSocket event: triage_rule.created broadcast on the workspace channel with { "id": "<rule id>" }.

PATCH /api/v1/triage-rules/{ruleId}

Partial update. Every field optional. Pass crew_id, assignee_id, or project_id as "" to NULL them out (the handler distinguishes “field absent” from “field set to empty string” and routes the empty string to SET column = NULL). Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role. Request body: When both pattern and match_type are present and match_type is regex, the new pattern is recompiled — invalid regex → 400. If only one of the two is supplied, no recompile happens, so changing match_type from containsregex without also resending the pattern can leave the rule with an unvalidated regex; the runtime Process path catches that and skips the rule rather than aborting the batch. Response: 200 OK with the full updated rule object. WebSocket event: triage_rule.updated broadcast on the workspace channel.

DELETE /api/v1/triage-rules/{ruleId}

Hard delete. Higher role bar than create / update because the rule is the source of truth for downstream automation. Request: No request body. The {ruleId} path parameter identifies the rule to delete.
Hard delete — the rule row is removed outright (no soft delete). The higher role bar reflects that the rule drives downstream automation.
Auth: authenticated session + workspace context + OWNER or ADMIN role (requireRole("manage")). Response: 204 No Content. WebSocket event: triage_rule.deleted broadcast on the workspace channel.

Process the backlog

One-shot endpoint that applies the rule set to the current backlog.

POST /api/v1/triage/process

Run all enabled rules against every unassigned BACKLOG issue in the workspace and apply the first-match update to each. Issues are selected with assignee_id IS NULL; a rerun therefore skips issues that received an assignee, but an issue matched by a rule that does not set assignee_id can be processed again. Request: JSON object {}; no path or query parameters. Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role (requireRole("create")).

Algorithm

  1. Load enabled rules ordered by position ASC. Regex patterns are pre-compiled once per call (so a single bad regex in the rule set logs a warning and skips that rule, but doesn’t abort the batch or re-compile per issue).
  2. Load all BACKLOG missions where assignee_id IS NULL and mission_type = 'issue' (sub-issues, missions, and crew missions are excluded).
  3. For each issue, walk the rule list and apply the first matching rule — set crew_id, assignee_id (with assignee_type = 'agent'), priority, and project_id from the rule. Subsequent rules are skipped for that issue even if they’d also match.
  4. Increment match_count on rules that matched.
  5. Broadcast triage.processed on the workspace channel with processed (total backlog scanned) and matched (rows mutated) — only when at least one match occurred.

Match-type semantics

labels_json is currently read but not applied to the issue inside Process — only crew_id, assignee_id/assignee_type, priority, and project_id are written. The column exists on the rule for forwards compatibility.

Response

If no rules are enabled the response is still 200 with { "processed": 0, "matched": 0 } (no SQL is run against missions).

See also

  • Issues — the missions table Process updates.
  • Crewscrew_id targets a crew row.
  • Agentsassignee_id is an agent id.
  • Recurring Issues — cron-driven issue creation; triage rules run on whatever lands in the backlog (manual or recurring).