Skip to content

ethanfarah/skillflow

Repository files navigation

SkillFlow

Workflow orchestration for Claude Code skills. Define pipelines as YAML, chain skills with control flow (while, if, foreach, parallel), and run headlessly via the Claude Code SDK.

Install

# Install globally from GitHub
npm install -g github:ethanfarah/skillflow

# Or clone and link locally
git clone https://github.com/ethanfarah/skillflow.git
cd skillflow
npm install
npm run build
npm link

After either method, the skillflow command is available globally.

Quick Start

# 1. Create a workflow file in your project (or anywhere)
cat > my-workflow.yaml << 'EOF'
name: quick-test
steps:
  - shell: "echo Hello from SkillFlow"
    id: greeting
  - prompt: "What is 2 + 2? Reply with just the number."
    id: math
    max_turns: 1
EOF

# 2. Run it
skillflow run my-workflow.yaml

# 3. Dry-run to see what would execute (no SDK/shell calls)
skillflow run my-workflow.yaml --dry-run

Using SkillFlow in Any Repo

SkillFlow runs from any directory. Point --cwd at your target repo, or just cd there first:

# Run a workflow against a specific repo
skillflow run ~/workflows/add-feature.yaml \
  -i "feature=Add dark mode support" \
  --cwd ~/dev/my-project

# Or cd into the repo first
cd ~/dev/my-project
skillflow run ~/workflows/add-feature.yaml \
  -i "feature=Add dark mode support"

Workflow files can live anywhere — in the target repo, in a shared ~/workflows/ directory, or in SkillFlow's own workflows/ folder.

Example: Feature Implementation Pipeline

name: add-feature
description: "Implement a feature, run tests, submit PR"

inputs:
  feature: { type: string, required: true }
  branch: { type: string, default: "main" }

steps:
  - prompt: "Implement this feature: {{ inputs.feature }}"
    id: impl
    max_turns: 50

  - shell: "npm test"
    id: tests
    on_error: continue

  - if:
      condition: "{{ tests.exit_code }} == 0"
      then:
        - skill: submit-pr
          id: pr
          args: "{{ inputs.branch }}"
      else:
        - prompt: "Fix the failing tests, then run them again"
          id: fix
skillflow run add-feature.yaml \
  -i feature="Add user avatar upload" \
  --cwd ~/dev/my-app \
  --verbose

Visual Composer (GUI)

SkillFlow includes a visual workflow editor for building and editing workflows in your browser.

Try it online →

Setup

The GUI must be built before use. If you installed globally from GitHub, the GUI is pre-built. If you cloned the repo locally:

npm run gui:build

Running

skillflow compose

This starts a local HTTP server (default port 3333) and opens the composer in your browser. You can then:

  • Drag blocks from the palette onto the canvas
  • Edit block properties in the right panel
  • Reorder blocks by dragging
  • Copy the generated YAML and save it as a workflow file

Options:

skillflow compose --port 8080   # use a different port
skillflow compose --no-open     # don't auto-open the browser

Note: If you're developing SkillFlow itself, use npm run gui:dev to run the Vite dev server with hot reload instead.

Block Types at a Glance

Block What it does Needs Claude?
shell Run a bash command No
prompt Ask Claude anything (freeform) Yes
skill Invoke a Claude Code skill (/submit-pr, /pr-review, etc.) Yes
extract Parse structured data from Claude's NL output Yes (Haiku)
if Conditional branch No
while Loop with condition No
foreach Iterate over an array No
parallel Run steps concurrently No
workflow Call another .yaml workflow No
worktree Git worktree create/cleanup lifecycle No
break Exit a loop early No

Each prompt and skill block gets a fresh, isolated Claude session with full autonomy. Data flows between steps via extract{{ step_id.field }} templates.

CLI Reference

skillflow run <workflow.yaml> [options]

Options:
  -i, --input KEY=VALUE   Set a workflow input (repeatable)
  --cwd PATH              Set working directory (default: current dir)
  --verbose               Stream full output live
  --quiet                 Silent, log to file only
  --log FILE              Write structured JSON event log
  --dry-run               Show what would execute without running
  -h, --help              Show help

skillflow compose [options]

  Opens the visual workflow composer in your browser (serves gui/dist/).
  Build the GUI first: npm run gui:build

Options:
  --port PORT             Server port (default: 3333)
  --no-open               Don't auto-open browser

Writing Workflows

Workflows are YAML files with a name, optional inputs/outputs, and a list of steps.

Minimal Example

name: hello-world
steps:
  - shell: echo "Hello from SkillFlow"
    id: greeting

Full Structure

name: my-workflow
description: "What this workflow does"

inputs:
  feature: { type: string, required: true }
  branch: { type: string, default: "main" }

outputs:
  result: "{{ final_step.output }}"

steps:
  # ... blocks go here

Block Types

skill - Invoke a Claude Code Skill

Runs a Claude Code skill (like /implement-feature, /submit-pr, etc.) in a fresh SDK session.

- skill: implement-feature
  id: impl
  args: "{{ inputs.feature }}"
  max_turns: 100         # optional, default 200
  timeout: 600000        # optional, ms, abort if exceeded
  cwd: /path/to/repo     # optional, default workflow cwd
  on_error: continue     # optional: fail | continue | retry

prompt - Freeform Claude Prompt

Sends a prompt to Claude with full tool access.

- prompt: "Analyze the test coverage in this repo and suggest improvements"
  id: analysis
  system_prompt: "You are a test coverage expert"  # optional
  allowed_tools: ["Read", "Glob", "Grep"]          # optional, restrict tools
  max_turns: 50                                     # optional
  timeout: 300000                                   # optional, ms

shell - Run a Shell Command

Executes a bash command directly (no SDK).

- shell: "npm test"
  id: tests
  cwd: "{{ worktree_path }}"  # optional
  timeout: 300000              # optional, ms, default 120000

Access results: {{ tests.stdout }}, {{ tests.stderr }}, {{ tests.exit_code }}

Security note: By default, {{ }} values in shell commands are interpolated as-is. If values come from untrusted sources, use safe_interpolate: true to automatically shell-escape all interpolated values:

- shell: "echo {{ inputs.user_input }}"
  safe_interpolate: true   # wraps resolved values in single quotes

extract - Parse Structured Data from Natural Language

The bridge between Claude's natural language outputs and your workflow's control flow. Uses a fast/cheap Claude call (Haiku) to extract structured JSON.

- extract:
    id: pr_info
    from: "{{ submit.output }}"
    schema:
      pr_number: { type: integer, description: "The PR number" }
      pr_url: { type: string, description: "The full PR URL" }

Access results: {{ pr_info.pr_number }}, {{ pr_info.pr_url }}

For while-loop seeding, use the "initial" sentinel with defaults:

- extract:
    id: status
    from: "initial"
    schema:
      should_continue: { type: boolean }
    default: { should_continue: true }

if - Conditional Branch

- if:
    condition: "{{ test_results.exit_code }} == 0"
    then:
      - skill: submit-pr
        args: main
    else:
      - prompt: "Fix the failing tests"

while - Loop with Condition

- while:
    id: retry_loop
    condition: "{{ status.needs_retry }}"
    max_iterations: 5    # safety limit, default 100
    init:                 # runs once before first condition check
      - extract:
          id: status
          from: "initial"
          schema: { needs_retry: { type: boolean } }
          default: { needs_retry: true }
    steps:
      - skill: fix-issues
      - extract:
          id: status
          from: "{{ fix_result.output }}"
          schema: { needs_retry: { type: boolean } }

foreach - Iterate Over an Array

- foreach:
    id: file_loop
    items: "{{ file_list.data.files }}"
    item_var: file       # default: "item"
    index_var: idx        # default: "index"
    steps:
      - prompt: "Review {{ file }}"

parallel - Concurrent Execution

- parallel:
    concurrency: 3        # optional, default: all at once
    steps:
      - shell: "npm test"
      - shell: "npm run lint"
      - shell: "npm run typecheck"

workflow - Call Another Workflow

Compose workflows with isolated scopes. Inputs go in, declared outputs come back. File paths are resolved relative to the parent workflow's directory.

- workflow:
    id: review_result
    file: "./review-cycle.yaml"      # resolved relative to this workflow's dir
    inputs:
      pr_number: "{{ pr_info.pr_number }}"

worktree - Git Worktree Lifecycle

Creates an isolated git worktree, runs steps inside it, auto-cleans up.

- worktree:
    id: work
    branch: "feature-x"
    base: "main"          # optional, default HEAD
    cleanup: true          # optional, default true
    steps:
      - skill: implement-feature
        args: "{{ inputs.feature }}"

break - Exit a Loop Early

- while:
    condition: "true"
    max_iterations: 100
    steps:
      - extract:
          id: check
          from: "{{ result.output }}"
          schema: { done: { type: boolean } }
      - if:
          condition: "{{ check.done }}"
          then:
            - break: true

Variable Interpolation

Use {{ expression }} to reference data. Expressions are resolved in this order:

  1. Loop locals - {{ item }}, {{ index }} (inside foreach)
  2. Inputs - {{ inputs.feature_description }}
  3. Environment - {{ env.HOME }}
  4. Step outputs - {{ step_id.field }}

Dot access navigates nested objects: {{ pr_info.pr_number }}

For extract results, you can access fields directly without .data: {{ pr_info.pr_number }} (not {{ pr_info.data.pr_number }}).

Single-expression templates preserve types: {{ items }} returns the actual array, not a stringified version. This is how foreach.items gets an iterable array.

Error Handling

Per-step error policy:

- skill: merge-pr
  on_error: continue     # swallow error, mark step as failed, keep going

- shell: "flaky-command"
  on_error: retry
  retry:
    max_attempts: 3
    delay_ms: 1000
    backoff: exponential  # none | linear | exponential

- skill: critical-step
  on_error: fail          # default - stop the workflow

If on_error: retry is set without a retry block, defaults to 3 attempts with 1s exponential backoff.

Condition Expressions

Conditions support comparison and logical operators. All {{ }} values are resolved before evaluation. No JavaScript eval() is used.

# Comparisons
condition: "{{ count }} > 0"
condition: "{{ status }} == 'approved'"
condition: "{{ a }} != {{ b }}"

# Logical operators
condition: "{{ x }} > 0 && {{ y }} < 10"
condition: "{{ flag }} || {{ fallback }}"
condition: "!{{ should_skip }}"

# Truthiness (bare value)
condition: "{{ has_issues }}"   # true/false, truthy/falsy check

# Parentheses
condition: "({{ a }} > 1) && ({{ b }} == 'yes' || {{ c }})"

Falsy values: false, 0, "", null, undefined, and their string equivalents.

Verbosity Modes

# Verbose: stream everything (all events logged to stderr)
skillflow run flow.yaml --verbose

# Normal (default): step start/end with durations
skillflow run flow.yaml

# Quiet: no stderr output, results only
skillflow run flow.yaml --quiet

# JSON event log (works with any verbosity)
skillflow run flow.yaml --log events.json

Cancellation

Workflows can be cancelled via AbortController:

const ac = new AbortController();

// Cancel after 5 minutes
setTimeout(() => ac.abort(), 5 * 60 * 1000);

const result = await runWorkflow(doc, {
  inputs: { feature: "Add dark mode" },
  abortSignal: ac.signal,
});

The CLI handles SIGINT (Ctrl+C) and SIGTERM automatically, aborting the running workflow gracefully.

YAML Validation

Workflow YAML files are validated at load time using Zod schemas. Typos and structural errors produce helpful messages:

Invalid workflow document:
  - steps.0: Each step must be an object with exactly one block-type key: skill, prompt, shell, extract, workflow, worktree, if, while, foreach, parallel, or break
  - steps.2.skilll: Unrecognized key(s) in object: 'skilll'

Programmatic API

import { loadWorkflow, runWorkflow } from "skillflow";

const doc = loadWorkflow("./my-workflow.yaml");

const result = await runWorkflow(doc, {
  inputs: { feature: "Add dark mode" },
  cwd: "/path/to/repo",
  workflowFile: "./my-workflow.yaml",  // enables relative child workflow resolution
  verbosity: "normal",
  logFile: "events.json",
  abortSignal: myAbortController.signal, // optional cancellation
});

console.log(result.succeeded);   // boolean
console.log(result.outputs);     // declared workflow outputs
console.log(result.duration_ms); // total runtime

Example: Plan to Finished PR

The included workflows/plan-to-finished-pr.yaml automates the full PR lifecycle:

  1. Implement the feature with /implement-feature
  2. Submit PR with /submit-pr
  3. Extract PR number and URL from the output
  4. Review loop: wait for review, check for issues, address them, repeat
  5. Merge the PR with /merge-pr
skillflow run workflows/plan-to-finished-pr.yaml \
  -i "feature_description=Add a search bar to the dashboard" \
  --cwd ~/dev/my-project \
  --log pr-log.json

Testing

npm test

Tests use Node's built-in test runner (node:test) and cover interpolation, conditions, context, executor, semaphore, retry, and YAML validation.

Architecture

src/
  schema/types.ts        - TypeScript types for all blocks, outputs, events
  schema/validate.ts     - Zod schema validation for YAML workflows
  runtime/
    executor.ts          - Recursive block interpreter (core engine)
    context.ts           - Variable store, {{ }} interpolation, scope forking
    condition.ts         - Safe expression evaluator (no eval)
  runners/
    skill-runner.ts      - skill -> Claude Code SDK query()
    prompt-runner.ts     - prompt -> SDK query()
    shell-runner.ts      - shell -> child_process
    extract-runner.ts    - extract -> Haiku SDK call for NL-to-JSON
    workflow-runner.ts   - workflow -> recursive load + execute
    worktree-runner.ts   - worktree -> git worktree lifecycle
  util/
    interpolation.ts     - {{ }} template parser
    sdk-env.ts           - SDK subprocess env (nested session fix)
    semaphore.ts         - Async concurrency limiter
    retry.ts             - Retry with backoff
    logger.ts            - Structured event logging
    errors.ts            - BreakSignal, WorkflowError, SkillError
  cli.ts                 - CLI entry point
  compose-server.ts      - HTTP server for the visual composer (skillflow compose)
  index.ts               - Public API
gui/                     - Visual workflow composer (React + Vite)
  src/
    App.tsx              - Root component, useReducer state, 3-panel layout
    lib/                 - state.ts, yaml-gen.ts, block-meta.ts, block-defaults.ts
    components/          - BlockCard, BlockEditor, BlockPalette, forms/, …

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors