Skip to content

How Workflows Work

A workflow is a directed acyclic graph (DAG) of nodes connected by edges, with a single entry point. Each node gives Claude an instruction and a set of skills (tools). Edges define the flow between nodes, optionally with natural-language conditions that Claude evaluates at runtime.

This is SWEny’s core abstraction. Instead of writing procedural automation scripts, you declare what should happen at each step and when to move between steps. Claude handles the execution.

Every workflow has four parts:

Field Purpose
id Unique identifier (used in CLI commands and exports)
name Human-readable display name
entry The node where execution begins
nodes A map of node definitions, each with an instruction and skills
edges Connections between nodes, optionally with when conditions

Here is the TypeScript interface from @sweny-ai/core:

interface Workflow {
id: string;
name: string;
description: string;
nodes: Record<string, Node>;
edges: Edge[];
entry: string;
}
interface Node {
name: string;
instruction: string;
skills: string[];
output?: JSONSchema;
}
interface Edge {
from: string;
to: string;
when?: string;
}

The executor walks the graph node-by-node, starting at the entry node, until it reaches a terminal node (one with no outgoing edges).

Step-by-step:

  1. Start at the entry node. The executor looks up workflow.entry in the node map.
  2. Build context. Claude receives the node’s instruction, the workflow input, and the accumulated results from all prior nodes.
  3. Resolve tools. The executor gathers tools from every skill listed in the node’s skills array.
  4. Claude executes. Claude runs the instruction, calling tools as needed (querying APIs, reading files, creating issues, etc.).
  5. Collect the result. Claude returns a NodeResult with a status (success, skipped, or failed), arbitrary data, and a record of all tool calls made.
  6. Route to the next node. The executor evaluates outgoing edges. If there is a single unconditional edge, it follows it. If there are conditional edges, Claude evaluates the when clauses against the current results and picks a path.
  7. Repeat until reaching a terminal node.
interface NodeResult {
status: "success" | "skipped" | "failed";
data: Record<string, unknown>;
toolCalls: ToolCall[];
}

Edges can have a when clause written in natural language. At runtime, Claude evaluates each condition against the current node’s result and picks the matching path.

edges:
- from: investigate
to: create_issue
when: "The issue is novel (not a duplicate) and severity is medium or higher"
- from: investigate
to: skip
when: "The issue is a duplicate of an existing ticket, or severity is low"

The executor presents all outgoing conditions as choices and asks Claude to pick one. If an edge has no when clause, it acts as a default/unconditional path.

Nodes can declare an output schema (JSON Schema). When present, Claude’s response is validated against the schema, producing structured data that downstream nodes and routing conditions can reference.

investigate:
name: Root Cause Analysis
instruction: "Classify every distinct issue as novel or duplicate..."
skills: [github, linear]
output:
type: object
properties:
findings:
type: array
items:
type: object
properties:
title: { type: string }
root_cause: { type: string }
severity: { type: string, enum: [critical, high, medium, low] }
is_duplicate: { type: boolean }
fix_complexity: { type: string, enum: [simple, moderate, complex] }
required: [title, root_cause, severity, is_duplicate]
novel_count: { type: number }
highest_severity: { type: string, enum: [critical, high, medium, low] }
recommendation: { type: string }
required: [findings, novel_count, highest_severity, recommendation]

This is how the triage workflow’s conditional routing works: the investigate node outputs a findings array where each item is classified as novel or duplicate. The novel_count and highest_severity fields drive edge conditions.

Pass dryRun: true (CLI: --dry-run, Action: dry-run: true) to run a workflow in analysis-only mode. A dry run never takes a write action, on any workflow shape.

  • Every node runs with read-only tools. Each skill tool is classified read or write. Under a dry run only read tools are handed to a node; write tools (create issue, add comment, open PR, send message, insert row) and any tool with no classification are withheld. Claude cannot call a tool it was never given.
  • No other write path. External MCP servers are not attached, including ones from your Claude settings, .mcp.json, or plugins (their tools are unclassified, so they count as writes). The shell, file-edit, subagent and URL-fetch built-ins (Bash, Write, Edit, WebFetch, …) are disallowed. Read, Grep, Glob and WebSearch stay available.
  • Withheld tools are recorded. Each node’s result lists them under skippedWrites, and the node is told which tools were withheld so it can describe what it would have written.
  • The run stops at the first natural-language conditional edge. Unconditional edges and expression edges are followed, since sweny evaluates an expression itself; at a node with natural-language when edges the run ends without asking Claude to pick a branch. This keeps dry-run routing free of model calls. It is not what makes a dry run safe; the read-only tools are.

This is enforced by the executor, not by prompt instructions.

In practice:

  • A linear workflow (for example fetch-diff → review-code → post-review) runs every node. The review is real; post-review reports the comment it would have posted, and nothing is posted.
  • Triage workflow: runs prepare → gather → investigate, then stops. You get the full investigation report but no issues are created, no PRs opened, no notifications sent.
  • Implement workflow: runs analyze, then stops. You get the analysis and fix plan but no code changes are made.

The executor emits events at every stage. Pass an observer function to receive them in real-time:

type ExecutionEvent =
| { type: "workflow:start"; workflow: string }
| { type: "node:enter"; node: string; instruction: string }
| { type: "node:progress"; node: string; message: string }
| { type: "tool:call"; node: string; tool: string; input: unknown }
| { type: "tool:result"; node: string; tool: string; output: unknown }
| { type: "node:exit"; node: string; result: NodeResult }
| { type: "route"; from: string; to: string; reason: string }
| { type: "workflow:end"; results: Record<string, NodeResult> };
type Observer = (event: ExecutionEvent) => void;

The CLI uses these events to render a live DAG visualization in your terminal. Studio uses them for live mode. You can also use them for logging, metrics, or custom integrations.

SWEny validates every workflow before execution. The validateWorkflow() function checks:

Rule Error code
Entry node must exist in the node map MISSING_ENTRY
All edge from values must reference existing nodes UNKNOWN_EDGE_SOURCE
All edge to values must reference existing nodes UNKNOWN_EDGE_TARGET
No edge may reference the same node as both source and target SELF_LOOP
All nodes must be reachable from the entry node (BFS) UNREACHABLE_NODE
Referenced skills must exist in the skill catalog (optional) UNKNOWN_SKILL

You can validate a workflow file without running it:

Terminal window
sweny workflow validate my-workflow.yml

SWEny ships with two built-in workflows:

  • Triage – investigate a production alert, determine root cause, create an issue, and notify the team.
  • Implement – analyze an issue, implement a fix, open a PR, and notify the team.

You can export either as YAML and use it as a starting point for customization:

Terminal window
sweny workflow export triage > my-triage.yml
sweny workflow export implement > my-implement.yml

Any workflow can be rendered as a Mermaid diagram – great for PR descriptions, runbooks, or wherever GitHub/GitLab render Mermaid natively:

Terminal window
# Fenced markdown, paste-ready
sweny workflow diagram my-workflow.yml
# Raw .mmd file for mmdc or the Mermaid Live Editor
sweny workflow diagram my-workflow.yml -o diagram.mmd

Diagrams on this site render with the SWEny brand: the current node is bold blue, passed nodes solid blue, failed red, skipped dashed slate, nodes not reached an outline. The same styles ship in every diagram SWEny prints for a run:

graph TB
survey[Survey the repo]
explain([Explain it ▶])
review[Review the answer]
survey --> explain
explain --> review
classDef current fill:#3b82f6,stroke:#1d4ed8,color:#fff,stroke-width:4px
classDef success fill:#2563eb,stroke:#1d4ed8,color:#fff,stroke-width:2px
classDef pending fill:none,stroke:#64748b,color:#64748b,stroke-width:1px
class survey success
class explain current
class review pending
linkStyle 0 stroke:#2563eb,stroke-width:3px
linkStyle 1 stroke:#64748b,stroke-width:1px,stroke-dasharray:5 5

See sweny workflow diagram for all options.