# fabro-workflow A DOT-based pipeline runner for multi-stage AI workflows. Define workflows as Graphviz `digraph` files and execute them with pluggable handlers, conditional routing, human-in-the-loop gates, parallel branching, retry policies, and checkpoint-based recovery. ## Key Concepts - **Graph** -- A directed graph parsed from DOT syntax containing nodes, edges, and attributes. The graph carries a `goal` describing the pipeline's purpose. - **Node** -- A workflow step. Graphviz shapes map to handler types (e.g., `Mdiamond` = start, `Msquare` = exit, `box` = agent, `tab` = prompt, `diamond` = conditional, `hexagon` = human gate, `component` = parallel). - **Edge** -- A connection between nodes with optional `condition`, `label`, `weight`, and `fidelity` attributes that control routing. - **Handler** -- An async trait implementation that executes a node and returns an `Outcome`. Built-in handlers include `StartHandler`, `ExitHandler`, `AgentHandler`, `PromptHandler`, `ConditionalHandler`, `HumanHandler`, `ParallelHandler`, `FanInHandler`, `CommandHandler`, and `SubWorkflowHandler`. - **Outcome** -- The result of executing a handler, carrying a `StageOutcome` (Success, Fail, PartialSuccess, Retry, Skipped), optional routing hints (`preferred_label`, `suggested_next_ids`), and context updates. - **Context** -- A thread-safe key-value store shared across pipeline stages, supporting snapshots and isolated cloning for parallel branches. - **Interviewer** -- A trait for human-in-the-loop interactions. Implementations include `AutoApproveInterviewer`, `QueueInterviewer`, `CallbackInterviewer`, `ConsoleInterviewer`, and `RecordingInterviewer`. - **Checkpoint** -- A serializable snapshot of execution state (completed nodes, context values) for crash recovery and resume. ## Pipeline Definition Pipelines are defined using Graphviz DOT syntax: ```dot digraph MyPipeline { graph [goal="Implement and validate a feature"] rankdir=LR node [shape=box, timeout="900s"] start [shape=Mdiamond, label="Start"] exit [shape=Msquare, label="Exit"] plan [label="Plan", prompt="Plan the implementation"] implement [label="Implement", prompt="Implement the plan"] validate [label="Validate", prompt="Run tests"] gate [shape=diamond, label="Tests passing?"] start -> plan -> implement -> validate -> gate gate -> exit [label="Yes", condition="outcome=succeeded"] gate -> implement [label="No", condition="outcome!=succeeded"] } ``` ## Usage ### Parsing and Validating a Pipeline ```rust use fabro_workflow::operations::{create, CreateOptions}; let dot_source = r#"digraph Simple { graph [goal="Run tests"] start [shape=Mdiamond] exit [shape=Msquare] work [shape=box, prompt="Run the test suite"] start -> work -> exit }"#; let validated = create(dot_source, CreateOptions::default()) .expect("pipeline should parse"); validated.raise_on_errors().expect("pipeline should validate"); let (graph, _, _) = validated.into_parts(); assert_eq!(graph.name, "Simple"); assert_eq!(graph.goal(), "Run tests"); ``` `operations::create` parses the DOT source, applies built-in transforms (variable expansion, stylesheet application, preamble injection), and returns diagnostics through `Validated`. ### Running a Pipeline ```rust use fabro_workflow::operations::start; use fabro_workflow::pipeline; // Use `operations::start(...)` for the full // initialize -> execute -> conclude -> publish -> finalize flow. // Use `pipeline::initialize(...)` + `pipeline::execute(...)` when you need partial lifecycle control. ``` ### Custom Handlers Implement the `Handler` trait to add custom node behavior: ```rust use arc_workflows::handler::Handler; use arc_workflows::context::Context; use arc_workflows::graph::{Graph, Node}; use arc_workflows::outcome::Outcome; use arc_workflows::error::ArcError; use async_trait::async_trait; use std::path::Path; struct MyHandler; #[async_trait] impl Handler for MyHandler { async fn execute( &self, node: &Node, context: &Context, graph: &Graph, run_dir: &Path, ) -> Result { // Custom logic here Ok(Outcome::success()) } } ``` ### Model Stylesheets CSS-like stylesheets control LLM model assignment with specificity-based cascading: ```dot digraph Styled { graph [ goal="Build feature", model_stylesheet=" * { model: claude-sonnet-4-5;} .code { model: claude-opus-4-6; } #critical_review { model: gpt-5.2;} " ] // ... } ``` Selectors by specificity: `*` (universal, 0) < `shape` (1) < `.class` (2) < `#id` (3). Explicit node attributes are never overridden. ### Condition Expressions Edge conditions use a simple expression syntax for routing: ``` outcome=succeeded outcome!=failed outcome=succeeded && context.tests_passed=true my_flag ``` Clauses support `=`, `!=`, and bare key truthiness checks, joined with `&&`. ### Human-in-the-Loop Gates Nodes with `shape=hexagon` or `type="human"` pause execution for human input. Outgoing edge labels become selectable options, with accelerator key parsing for patterns like `[A] Approve` and `F) Fix`. ### Parallel Execution Nodes with `shape=component` fan out to branches concurrently. Branches receive isolated context forks, share the same sandbox checkout, and always finish before the workflow continues. Use `max_parallel` to limit concurrency; concurrent workspace writes are user-managed. ### Checkpoints and Resume The engine saves a checkpoint after each node. Resume from a checkpoint with `engine.run_from_checkpoint(&graph, &config, &checkpoint)`. ## Architecture ``` parser (DOT -> AST -> Graph) -> transform (variable expansion, stylesheet, preamble) -> validation (14 lint rules) -> engine (execution loop with retry, edge selection, goal gates) -> handler (pluggable node executors) -> interviewer (human-in-the-loop I/O) ```