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.
Overview
Section titled “Overview”prepare --> gather --> investigate --[novel + medium+]--> create_issue --[feasible fix]--> implement --> create_pr --> notify \ \ \ --[complex fix]--> notify \ --[all duplicates or low]--> skip --> notifyEight nodes, three conditional branches:
- prepare – fetch rules and context documents from configured URLs
- gather – pull error details, recent commits, and past issues from all available tools
- investigate – classify each issue as novel or duplicate; assess severity and fix complexity
- create_issue – file new tickets for novel findings, +1 existing tickets for duplicates (only if novel issues exist at medium+ severity)
- skip – +1 duplicate tickets and log low-priority findings (when no novel issues worth acting on)
- implement – write the fix (only if at least one finding has a feasible fix approach)
- create_pr – push branch and open a pull request
- notify – send a summary to the team’s notification channel
Workflow definition
Section titled “Workflow definition”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 rulesand 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 issuewith 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 fixthe 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 details
Section titled “Node details”| 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 output schema
Section titled “The investigate output schema”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 |
Conditional routing
Section titled “Conditional routing”The workflow has three conditional branch points:
After investigate
Section titled “After investigate”| 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.
After create_issue
Section titled “After create_issue”| 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).
Duplicate detection
Section titled “Duplicate detection”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.
Provider-agnostic design
Section titled “Provider-agnostic design”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 |
Permissions and writes
Section titled “Permissions and writes”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):
gatherandinvestigatekeep 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.
Running the triage workflow
Section titled “Running the triage workflow”From the CLI:
sweny triage --dry-run # investigate without creating issuessweny triage # full run: investigate + create issues + notifyFrom 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:
sweny workflow export triage > my-triage.yml# Edit the YAML, then run:sweny workflow run my-triage.ymlWhat’s next?
Section titled “What’s next?”- Implement Workflow – the built-in issue-to-PR workflow
- Custom Workflows – modify the triage workflow or build your own
- YAML Reference – full schema for workflow files