A KDL-based workflow runner that orchestrates multi-stage AI workflows. Each stage is an AI/tool/human task and transitions define execution flow.
Built from the StrongDM Software Factory NLSpecs:
- Attractor Specification — Pipeline engine
- Coding Agent Loop Specification — Provider-aligned agentic loop
- Unified LLM Client Specification — Multi-provider LLM SDK
┌─────────────────────────────────────────┐
│ CLI │ Standalone CLI entry point
│ (src/cli.ts) │ (run, validate, show)
├─────────────────────────────────────────┤
│ Pi Extension │ /attractor slash command,
│ (src/extensions/) │ TUI panel, interactive interviewer
├─────────────────────────────────────────┤
│ PiBackend │ CodergenBackend powered by pi SDK
│ (src/pi-backend.ts) │ (AgentSession, tool modes, usage)
├─────────────────────────────────────────┤
│ Pipeline Engine │ KDL parser, execution engine,
│ (src/pipeline/) │ handlers, conditions, validators
└─────────────────────────────────────────┘
workflow "FeaturePipeline" {
version 2
goal "Implement and validate a feature"
start "plan"
stage "plan" kind="llm" prompt="Plan the implementation for: $goal"
stage "implement" kind="llm" prompt="Implement the plan" goal_gate=true
stage "validate" kind="llm" prompt="Run tests and verify correctness"
stage "gate" kind="decision" {
route when="outcome(\"validate\") == \"success\"" to="exit"
route when="true" to="implement"
}
stage "exit" kind="exit"
transition from="plan" to="implement"
transition from="implement" to="validate"
transition from="validate" to="gate"
}import { parseWorkflowKdl, workflowToGraph, validateWorkflowOrRaise, runPipeline } from "attractor";
const kdl = fs.readFileSync("pipeline.awf.kdl", "utf-8");
const workflow = parseWorkflowKdl(kdl);
validateWorkflowOrRaise(workflow);
const graph = workflowToGraph(workflow);
const result = await runPipeline({
graph,
logsRoot: "./logs",
backend: myCodergenBackend, // Your LLM integration
onEvent: (event) => console.log(event.kind, event.data),
});| Kind | Handler | Description |
|---|---|---|
llm |
codergen |
LLM task with $goal variable expansion |
exit |
exit |
Pipeline exit point (no-op, goal gate check) |
human |
wait.human |
Human-in-the-loop gate |
decision |
conditional |
Routing based on edge conditions |
tool |
tool |
External tool execution |
workspace.create |
workspace.create |
Create an isolated jj workspace |
workspace.merge |
workspace.merge |
Merge a workspace back |
workspace.cleanup |
workspace.cleanup |
Clean up a workspace |
5-step deterministic priority: condition match → preferred label → suggested IDs → weight → lexical tiebreak.
stage "gate" kind="decision" {
route when="outcome(\"validate\") == \"success\"" to="deploy"
route when="outcome(\"validate\") == \"fail\"" to="fix"
route when="outcome(\"validate\") == \"success\" && context(\"tests_passed\") == \"true\"" to="deploy"
}Stages with goal_gate=true must succeed before the pipeline can exit.
KDL workflows configure per-stage models via named profiles:
workflow "example" {
version 2
start "plan"
models {
default "fast"
profile "fast" model="claude-sonnet-4-5"
profile "heavy" model="claude-opus-4-6" reasoning_effort="high"
}
stage "plan" kind="llm" prompt="Plan it" model_profile="fast"
stage "implement" kind="llm" prompt="Build it" model_profile="heavy"
stage "done" kind="exit"
transition from="plan" to="implement"
transition from="implement" to="done"
}Execution state saved after each node. Resume from any checkpoint.
Built-in interviewers: AutoApproveInterviewer, QueueInterviewer, CallbackInterviewer, RecordingInterviewer.
src/
├── cli.ts Standalone CLI entry point
├── cli-renderer.ts CLI output rendering
├── pi-backend.ts CodergenBackend powered by pi SDK
├── interactive-interviewer.ts Interactive human gate prompts
├── index.ts Public API re-exports
├── pipeline/ Pipeline Engine
│ ├── types.ts Graph model, context, handlers, interviewers
│ ├── workflow-types.ts KDL workflow definition types
│ ├── workflow-kdl-parser.ts KDL workflow parser
│ ├── workflow-loader.ts Workflow-to-graph conversion
│ ├── workflow-resolution.ts Shared workflow discovery & resolution
│ ├── workflow-validator.ts Workflow-level validation
│ ├── workflow-expr.ts Workflow expression evaluator
│ ├── validator.ts Graph-level lint rules
│ ├── conditions.ts Edge condition expression language
│ ├── engine.ts Core execution loop, edge selection, checkpoints
│ ├── handlers.ts Node handlers + registry
│ ├── interviewers.ts Human-in-the-loop implementations
│ ├── workspace.ts Jujutsu workspace handlers
│ ├── tool-failure.ts Structured tool failure details
│ ├── status-markers.ts Stage status file utilities
│ └── graph-to-dot.ts Graph → DOT export
└── extensions/ Pi extension integration
├── attractor.ts Main extension entry point
├── attractor-command.ts Pipeline CLI commands
├── attractor-interviewer.ts Interactive interviewer
└── attractor-panel.ts TUI panel rendering
Attractor ships a standalone CLI for running, validating, and visualizing workflows.
npm install -g attractor # or use npx attractor# Run a pipeline (path or bare name)
attractor run pipeline.awf.kdl [options]
attractor run deploy # resolves to .attractor/workflows/deploy.awf.kdl
# Validate a workflow graph
attractor validate pipeline.awf.kdl
# Visualize a workflow graph (ASCII/boxart via graph-easy, or raw DOT)
attractor show pipeline.awf.kdl [--format ascii|boxart|dot]
# List available LLM models
attractor list-models [--provider anthropic]| Flag | Description |
|---|---|
--goal <text> |
Override the graph's goal attribute |
--model <model> |
LLM model to use (default: claude-opus-4-6) |
--provider <name> |
Provider name (default: anthropic) |
--logs <dir> |
Logs directory (default: .attractor/logs) |
--system <prompt> |
System prompt for codergen stages |
--tools <mode> |
Tool mode: none, read-only, coding (default: coding) |
--approve-all |
Auto-approve all human gates (no interactive prompts) |
--resume [checkpoint] |
Resume from checkpoint (default: <logs>/checkpoint.json) |
--dry-run |
Validate and print graph without executing |
--verbose |
Show detailed event output |
The CLI uses pi's AuthStorage for credentials. Either:
- Set
ANTHROPIC_API_KEYin your environment, or - Run
pi /loginto authenticate with a Claude subscription
attractor run feature.awf.kdl --goal "Add user authentication" --tools coding --verboseThe CLI renders a live spinner per stage, shows per-stage model overrides, and prints a usage/cost summary at completion.
Attractor integrates with the pi coding agent as a /attractor slash command, providing an interactive TUI experience for pipeline execution.
Place the extension entry point in your project:
.pi/extensions/attractor.ts
// Re-exports the built attractor extension for pi auto-discovery
export { default } from "../../dist/extensions/attractor.js";Then reload pi (/reload) to pick up the extension.
/attractor run <workflow> --goal "..." [options]
/attractor validate <workflow>
/attractor show <workflow> [--format ascii|boxart|dot]
Both the CLI and extension use a shared resolution module. Workflow references are resolved in order:
- Direct file path — absolute or relative to cwd
- Bare name — resolved by filename stem from known Attractor locations:
<cwd>/.attractor/workflows/<name>.awf.kdl(project-local)<repo-root>/.attractor/workflows/<name>.awf.kdl(repo-level, when cwd is a subdir)~/.attractor/workflows/<name>.awf.kdl(global user-level)
Project-local workflows take precedence over repo-root, which takes precedence over global. When duplicate filenames exist across locations, the higher-precedence copy wins and a warning is emitted.
So /attractor run deploy or attractor run deploy will search these locations for deploy.awf.kdl.
| Flag | Description |
|---|---|
--goal <text> |
Pipeline goal (required unless the graph has one) |
--resume |
Resume from last checkpoint |
--approve-all |
Auto-approve all human gates |
--logs <dir> |
Logs directory (default: .attractor/logs) |
--tools <mode> |
Tool mode: none, read-only, coding |
--dry-run |
Validate and print graph without executing |
- Interactive interviewer — human gate prompts appear inline in the pi TUI
- Live panel — stage progress, agent tool calls, and text streaming rendered via custom message types
- Stage result rendering — success/failure cards with elapsed time and error details in the conversation area
- Tab completion — subcommand suggestions when typing
/attractor
This repo is managed with Nix flakes. Enter the dev shell first:
nix develop # or use direnv (auto-loads .envrc)Then:
npm install
npm run build
npm run lint # type-check
npm test