Aizen
A pipeline shouldn’t be a script. It should be a plan you can inspect, pause, resume, and rewire mid-flight.
Most automation tools treat pipelines as linear scripts. You write steps in order, run the thing, and hope nothing breaks halfway through. If it does, you fix the issue and restart from zero. That works for simple CI but the moment your pipeline involves AI prompts, shell commands, MCP server calls, approval gates, and parallel execution, the script model falls apart.
Aizen is a DAG-based workflow engine that treats each pipeline as a directed graph of stages not a flat list. It ships with multi-AI backends, a plugin system, checkpoint/resume, variable interpolation, streaming AI output, and a CLI designed for both interactive and headless use.
The project is written in Python and lives at github.com/J0na555/aizen.
The Shape of a Workflow
Workflows are defined as YAML. Each stage has an ID, a type, optional dependencies, and type-specific configuration. Here’s the minimal form:
name: "hello-world"
stages:
- id: greet
type: shell
command: "echo Hello, World!"From there it scales to complex DAGs with parallel branches, AI stages that chain output to one another, and failure strategies that decide whether to stop, continue, or retry.
A more realistic example planning and implementing a feature using AI, then linting, testing, and deploying:
name: "Build and deploy feature"
variables:
feature: "dark-mode-toggle"
stages:
- id: plan
type: ai
prompt: "Analyze the requirements for {{ feature }}. Create a plan."
output: plan.md
requires_approval: true
- id: implement
type: ai
prompt: "Implement {{ feature }} following plan.md"
depends_on: [plan]
- id: lint
type: shell
command: "ruff check src/"
depends_on: [implement]
on_fail: continue
- id: test
type: shell
command: "pytest tests/ -v"
depends_on: [implement]
on_fail: retry
max_retries: 2
- id: deploy
type: shell
command: "git push && gh workflow run deploy.yml"
depends_on: [lint, test]
requires_approval: trueThe engine resolves the dependency graph into execution waves. Stages in the same wave with no interdependencies run concurrently when --parallel is passed. The lint and test stages above, for example, execute in parallel after implement completes.
Stage Types
Aizen supports five built-in stage types, each with its own runner:
shell— Executes a shell command and captures stdout/stderr. Supports env vars, timeouts, and cwd configuration.ai— Sends a prompt to an AI CLI. Backends are auto-detected fromPATH— Claude, OpenCode, Codex, and Gemini are all supported. The engine dispatches to whichever is available, falling back through a configurable priority chain.mcp— Calls an MCP (Model Context Protocol) server via HTTP POST. Useful for connecting external tools, APIs, or microservices as pipeline stages.python— Imports a dotted Python path (module.function) and calls it with the stage state. For when you need programmatic logic inline.plugin— Runs a custom stage class installed in~/.aizen/plugins/. Plugins are discovered automatically, no registration needed.
Architecture
Aizen splits state across two domains: a global config directory and per-project directories.
~/.aizen/
config.yaml # API keys, project registry
plugins/ # Installed stage plugins
project/
.aizen/
config.yaml # Per-project settings
state.json # Current workflow state (checkpoint)
runs/ # Archived workflow runs
workflows/
feature.yaml # Workflow definitions
The engine itself is a loop: inspect the DAG for ready stages, dispatch each to its runner, checkpoint state, repeat. Each successful — or failed — stage triggers a state save to .aizen/state.json. If you hit Ctrl+C, the engine finishes the current stage and serializes everything. Resume later with aizen resume, and it picks up exactly where it left off.
The Plugin System
Plugins are Python classes that extend BaseStage. A plugin lives in ~/.aizen/plugins/ as either a single .py file or a package directory with plugin.json metadata.
from aizen.stages.base import BaseStage
class GreeterStage(BaseStage):
def run(self, stage, state, context=None):
name = stage.variables.get("name", "world")
state.output = f"Hello, {name}!"
state.status = "COMPLETED"
return stateThe hook system lets plugins observe execution without becoming stages. Hook points include before_stage, after_stage, on_failure, on_start, and on_complete. A plugin’s register_hooks() function is auto-detected and called on discovery.
AI Backend Auto-Detection
Rather than hardcoding a single AI provider, Aizen scans PATH for available CLIs and registers whichever it finds. The registry resolves the provider name claude, opencode, codex, gemini — to a client that wraps the CLI with subprocess.
Each client implements a simple interface:
class AIClient:
def run(self, prompt, model=None, context=None) -> str: ...
def run_streaming(self, prompt, model=None, context=None) -> Generator[str]: ...The streaming variant uses subprocess.Popen and yields lines as the AI produces them, rendered through Rich’s Live display for a real-time terminal experience.
Checkpoint, Resume, Rollback
This is where Aizen differentiates itself from a simple script runner.
After every stage — regardless of success or failure — the engine writes the full workflow state to disk. This means you can:
- Pause mid-pipeline (Ctrl+C or
aizen pausefrom another terminal). The engine waits for the current stage to finish, then saves. - Resume from exactly where you stopped. No re-execution of completed stages.
- Rollback a specific stage.
aizen rollback planresetsplanand every stage that depends on it to PENDING, while leaving upstream stages intact. - Edit a stage’s YAML mid-run with
aizen edit plan, which opens$EDITOR, validates the changes, and patches the workflow file.
This makes Aizen practical for long-running AI-heavy workflows where you want to iterate on prompts without starting over.
Variable Interpolation
Stages can reference each other’s output through ${...} expressions. The interpolation engine resolves these at runtime, just before a stage executes:
| Expression | Resolves To |
|---|---|
${stages.<id>.output} | Output of another completed stage |
${stages.<id>.error} | Error message of a failed stage |
${variables.<key>} | A runtime variable from the workflow |
${stage.<field>} | A field on the current stage |
This enables output chaining pass an AI stage’s generated plan directly into the prompt of a downstream implementation stage, without intermediate files.
Testing
77 tests covering models, state management, the DAG engine, config, plugins, workflow validation, parallel execution, variable interpolation, streaming, logging, dry-run, and integration. Tests use pytest with tmp_path fixtures for isolated filesystem state.
pytest tests/ -vKey Design Decisions
Some decisions shaped the project more than others:
YAML for workflows over GUI. Workflow definitions should be version-controlled, diffable, and reviewable in pull requests. A GUI would add maintenance overhead without proportional value for the target audience (developers).
Checkpoint after every stage. Granularity is cheap when state is serializable. Writing after every stage makes pause/resume trivial and loses at most one stage of progress on crash.
Plugin auto-discovery. Scanning ~/.aizen/plugins/ for BaseStage subclasses removes friction. No central plugin registry, no manual pip install steps for simple stages.
AI backends as CLI wrappers. Rather than calling APIs directly (which would require API keys in config and SDK dependencies per provider), Aizen shells out to whatever AI CLI the user already has installed. This keeps the dependency footprint small and leverages the user’s existing auth setup.
Thread-based parallelism, not async. Python’s ThreadPoolExecutor with a threading.Lock for shared state is simple, debuggable, and sufficient for I/O-bound stages. Async would add complexity without meaningful throughput gains for this workload pattern.
What’s Next
The v2 plan includes UI mode (terminal dashboard or TUI), dynamic stage injection (add stages mid-run based on AI output), conditional branching (if/then/else in workflows), and a workflow editor (visual DAG builder). The plugin system also opens the door for community-contributed integrations — Slack notifiers, Jira ticket creators, database migration runners.
Aizen is open source. The code, documentation, and all 77 passing tests are at github.com/J0na555/aizen.