Skip to content

Triage Workflow

The triage workflow is SWEny’s built-in pipeline for investigating production alerts. It follows a prepare, gather context, analyze root cause, create issues, implement fix, open PR, notify team pattern with conditional routing based on novelty and severity.

prepare --> gather --> investigate --[novel + medium+]--> create_issue --[feasible fix]--> implement --> create_pr --> notify
\ \
\ --[complex fix]--> notify
\
--[all duplicates or low]--> skip --> notify

Eight nodes, three conditional branches:

  1. prepare – fetch rules and context documents from configured URLs
  2. gather – pull error details, recent commits, and past issues from all available tools
  3. investigate – classify each issue as novel or duplicate; assess severity and fix complexity
  4. create_issue – file new tickets for novel findings, +1 existing tickets for duplicates (only if novel issues exist at medium+ severity)
  5. skip – +1 duplicate tickets and log low-priority findings (when no novel issues worth acting on)
  6. implement – write the fix (only if at least one finding has a feasible fix approach)
  7. create_pr – push branch and open a pull request
  8. notify – send a summary to the team’s notification channel

This is a simplified version of the definition from @sweny-ai/core (full instructions are longer in the source):

import type { Workflow } from "../types.js";
export const triageWorkflow: Workflow = {
id: "triage",
name: "Alert Triage",
description:
"Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team",
entry: "prepare",
nodes: {
prepare: {
name: "Load Rules & Context",
instruction: `Fetch and review any knowledge documents listed in the input.
Check input for rules_urls and context_urls arrays. Fetch each URL
(Linear docs via MCP, HTTP URLs directly). Summarize the key rules
and context for downstream nodes. No-op if no URLs are provided.`,
skills: ["linear"],
},
gather: {
name: "Gather Context",
instruction: `Gather all relevant context:
1. Observability: error details, stack traces, logs, metrics, incidents.
2. Source control: recent commits, PRs, deploys.
3. Issue tracker: similar past issues or known problems.
Be thorough — the investigation step depends on complete context.`,
skills: ["github", "sentry", "datadog", "betterstack", "linear"],
},
investigate: {
name: "Root Cause Analysis",
instruction: `Classify every distinct issue as novel or duplicate.
For each: identify root cause, assess severity and fix complexity,
search BOTH open AND closed issues for duplicates.
Output a findings array with novel_count and highest_severity.`,
skills: ["github", "linear"],
output: {
// see "The investigate output schema" below
},
},
create_issue: {
name: "Create Issues & Triage Duplicates",
instruction: `For each NOVEL finding: create a new issue with root cause,
severity, and fix approach. For each DUPLICATE: +1 the existing issue
with a confirmation comment.`,
skills: ["linear", "github"],
},
skip: {
name: "Skip — All Duplicates or Low Priority",
instruction: `All findings are duplicates or low priority.
+1 each duplicate's existing ticket. Log why low-priority items were skipped.`,
skills: ["linear", "github"],
},
implement: {
name: "Implement Fix",
instruction: `Create a feature branch, make minimal code changes to fix
the identified bug, run tests if available, and commit.`,
skills: ["github"],
},
create_pr: {
name: "Open Pull Request",
instruction: `Push the branch and open a PR with a clear title,
summary, and link to the issue.`,
skills: ["github"],
},
notify: {
name: "Notify Team",
instruction: `Send a notification summarizing the triage result.
Include: severity, root cause, and links to created issues or PRs.`,
skills: ["slack", "notification"],
},
},
edges: [
{ from: "prepare", to: "gather" },
{ from: "gather", to: "investigate" },
{
from: "investigate",
to: "create_issue",
when: { expr: "investigate.novel_count > 0 && investigate.highest_severity in ['medium', 'high', 'critical']" },
},
{
from: "investigate",
to: "skip",
when: { expr: "investigate.novel_count == 0 || investigate.highest_severity == 'low'" },
},
{
from: "create_issue",
to: "implement",
when: "at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided",
},
{
from: "create_issue",
to: "notify",
when: "all novel findings have fix_complexity complex, OR no clear fix_approach",
},
{ from: "skip", to: "notify" },
{ from: "implement", to: "create_pr" },
{ from: "create_pr", to: "notify" },
],
};
Node Name Skills Structured output?
prepare Load Rules & Context linear No
gather Gather Context github, sentry, datadog, betterstack, linear No
investigate Root Cause Analysis github, linear Yes
create_issue Create Issues & Triage Duplicates linear, github No
skip Skip — All Duplicates or Low Priority linear, github No
implement Implement Fix github No
create_pr Open Pull Request github No
notify Notify Team slack, notification No

The investigate node produces a findings array — one entry per distinct issue found. Each finding is independently classified as novel or duplicate. The novel_count and highest_severity fields drive routing.

{
"findings": [
{
"title": "NullPointerException in PaymentService.processRefund",
"root_cause": "Missing null check on refund.metadata after API v2 migration",
"severity": "high",
"affected_services": ["payment-api"],
"is_duplicate": false,
"duplicate_of": null,
"fix_approach": "Add null guard in processRefund() before accessing metadata.refundId",
"fix_complexity": "simple"
},
{
"title": "Slow query timeout on /api/orders endpoint",
"root_cause": "Missing index on orders.created_at after schema migration",
"severity": "medium",
"is_duplicate": true,
"duplicate_of": "ENG-456",
"fix_approach": "Add composite index on (user_id, created_at)",
"fix_complexity": "simple"
}
],
"novel_count": 1,
"highest_severity": "high",
"recommendation": "Create issue for the payment bug, +1 the orders query issue"
}
Field Type Required Description
findings array Yes All issues found (novel and duplicate)
findings[].title string Yes Short description
findings[].root_cause string Yes What caused it
findings[].severity critical | high | medium | low Yes Impact level
findings[].affected_services string[] No Impacted services
findings[].is_duplicate boolean Yes Whether this matches an existing ticket
findings[].duplicate_of string No Existing issue ID/URL if duplicate
findings[].fix_approach string No How to fix it
findings[].fix_complexity simple | moderate | complex No Estimated fix effort
novel_count number Yes Count of non-duplicate findings
highest_severity critical | high | medium | low Yes Highest severity across all findings
recommendation string Yes What should happen next

The workflow has three conditional branch points:

Target Condition
create_issue novel_count > 0 AND highest_severity is medium or higher
skip novel_count == 0 OR highest_severity is low

Both are expression conditions: SWEny evaluates them over the declared investigate output with no model call.

Target Condition
implement At least one novel finding has fix_complexity simple or moderate AND a fix_approach
notify All novel findings are complex, or no clear fix approach

The skip node always routes to notify (unconditional).

The novelty check is a critical part of the investigate node. Claude must search both open and closed issues in the issue tracker using multiple keyword variations. A finding is a duplicate if an existing issue covers the same root cause, error pattern, or affected service. This prevents SWEny from filing the same ticket repeatedly.

The triage workflow lists all compatible skills per category in each node. At runtime, only the skills that are actually configured (with valid credentials) are available. This means the same workflow works whether you use Datadog or Sentry for observability, Linear or GitHub Issues for tracking, and Slack or Discord for notifications.

Category Skills Purpose
git github Recent commits, PRs, deploys
observability sentry, datadog, betterstack Error details, logs, metrics
tasks linear, github Past issues, ticket creation
notification slack, notification Team alerts

Triage runs on sweny’s own opinions (permissions and safe outputs):

Node Access What it may write
gather, investigate read Nothing. Read skill tools only: no write tool, no file edits, and no shell (on Codex, a shell confined to its OS read-only sandbox: no writes, no network).
create_issue read + outputs Issues (at most 10), +1 comments (at most 10) and reopening closed duplicates (at most 10), as safe outputs.
skip read + outputs +1 comments and reopening closed duplicates (at most 10 each), as safe outputs.
implement write, no web The code change: file edits, tests, a local commit. No GitHub write tool.
create_pr write + outputs Pushes the branch (git needs a shell); the PR itself (at most 1) is a safe output.
notify write The notification.

The workflow caps the run at 25 writes and allows only issue, comment, issue_state and pr. The agent requests each issue, comment and PR with emit_output; sweny checks it and files it after the step, through the github or linear skill. The next steps read the new issue’s identifier and URL from context.create_issue.safe_outputs.

sweny triage pins every write to the tracker and repository you configured. When a provider cannot work that way, only the affected nodes keep their older write-capable shape, and the run prints which and why:

  • A Jira or file tracker, or GitLab source control: the nodes that write there use the tracker’s own tools.
  • An integration a read-only step cannot reach (Jira, GitLab, an observability provider without a built-in skill such as Loki or New Relic, your own MCP servers or workspace tools): gather and investigate keep the shell and MCP, with file edits and skill write tools denied.

sweny triage --stage previews every issue, comment and PR instead of filing it. --dry-run withholds every write tool. In both, sweny blocks the push rather than trusting the agent to skip it: git push fails in every node, and write tokens (GITHUB_TOKEN, GH_TOKEN) are withheld from the agent, so gh pr create fails too. The block is environment variables and git hooks: a deliberate agent can bypass it unless the run is sandboxed. In CI with the sandbox on and no push credential in the agent’s reach, it cannot push. See No push under –stage.

Behavior notes: a closed duplicate gets its +1 comment and is reopened (an issue_state output, reopen only). On Codex, read-only steps keep a shell confined to the OS read-only sandbox (no writes, no network), so gather and investigate can read the checkout.

From the CLI:

Terminal window
sweny triage --dry-run # investigate without creating issues
sweny triage # full run: investigate + create issues + notify

From GitHub Actions:

- uses: swenyai/triage@v1
with:
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
dd-api-key: ${{ secrets.DD_API_KEY }}
dd-app-key: ${{ secrets.DD_APP_KEY }}

Export and customize:

Terminal window
sweny workflow export triage > my-triage.yml
# Edit the YAML, then run:
sweny workflow run my-triage.yml