Skip to main content

Overview

The blueprint engine provides deterministic orchestration of Shell and Agent steps with conditional execution, error handling, and context flow between steps. Key Concepts:
  • Blueprint: Named sequence of steps
  • Step: Single unit of work (Shell or Agent)
  • StepContext: State passed between steps (working dir, last output, exit code, metadata)
  • Condition: Predicate evaluated before running a step
  • BlueprintRunner: Executes blueprints step-by-step

Core Types

Blueprint

A named sequence of steps.
pub struct Blueprint {
    pub name: String,
    pub steps: Vec<Step>,
}

Methods

new()
Creates a new empty blueprint.
pub fn new(name: impl Into<String>) -> Self
name
impl Into<String>
required
Blueprint name for logging (e.g. “magpie-tdd”, “demo-blueprint”)
add_step()
Adds a step to the blueprint (builder pattern).
pub fn add_step(mut self, step: Step) -> Self
step
Step
required
Step to append to the sequence
Self
Blueprint
Returns self for chaining
Example:
use magpie_core::{Blueprint, Step, StepKind, ShellStep, Condition};

let bp = Blueprint::new("my-workflow")
    .add_step(Step {
        name: "list-files".to_string(),
        kind: StepKind::Shell(ShellStep::new("ls").with_args(vec!["-la".to_string()])),
        condition: Condition::Always,
        continue_on_error: false,
    })
    .add_step(Step {
        name: "check-cargo".to_string(),
        kind: StepKind::Shell(ShellStep::new("cargo").with_args(vec!["check".to_string()])),
        condition: Condition::IfExitCode(0),
        continue_on_error: false,
    });

Step

A single step in a blueprint.
pub struct Step {
    pub name: String,
    pub kind: StepKind,
    pub condition: Condition,
    pub continue_on_error: bool,
}
name
String
required
Step name for logging (e.g. “scan-repo”, “run-tests”)
kind
StepKind
required
What the step does: Shell or Agent
condition
Condition
required
When to run this step (e.g. Always, IfExitCode(0))
continue_on_error
bool
required
If true, pipeline continues even if this step fails. If false, pipeline stops on error.

StepKind

What kind of work a step performs.
pub enum StepKind {
    Shell(ShellStep),
    Agent(AgentStep),
}
  • Shell: Runs a deterministic shell command
  • Agent: Runs an AI agent with tool access

StepContext

State passed between blueprint steps.
pub struct StepContext {
    pub working_dir: PathBuf,
    pub last_output: Option<String>,
    pub last_exit_code: Option<i32>,
    pub metadata: HashMap<String, String>,
}
working_dir
PathBuf
required
Working directory for shell commands and agent tools
last_output
Option<String>
Combined stdout + stderr from the previous step
last_exit_code
Option<i32>
Exit code from the previous step (0 = success)
metadata
HashMap<String, String>
Key-value store for passing data between steps (e.g. chat_history, trace_dir)

Methods

new()
Creates a new context with empty state.
pub fn new(working_dir: PathBuf) -> Self
Example:
use magpie_core::StepContext;
use std::path::PathBuf;

let mut ctx = StepContext::new(PathBuf::from("/workspace"));
ctx.metadata.insert("chat_history".to_string(), "...".to_string());

Condition

Condition evaluated by the engine before running a step.
pub enum Condition {
    Always,
    IfExitCode(i32),
    IfExitCodeNot(i32),
    IfOutputContains(String),
}
  • Always: Step always runs
  • IfExitCode(code): Step runs only if previous exit code matches
  • IfExitCodeNot(code): Step runs only if previous exit code doesn’t match
  • IfOutputContains(needle): Step runs only if previous output contains the string

Methods

evaluate()
Evaluates the condition against current context.
pub fn evaluate(&self, ctx: &StepContext) -> bool
Example:
use magpie_core::{Condition, StepContext};
use std::path::PathBuf;

let mut ctx = StepContext::new(PathBuf::from("/workspace"));
ctx.last_exit_code = Some(0);

assert!(Condition::Always.evaluate(&ctx));
assert!(Condition::IfExitCode(0).evaluate(&ctx));
assert!(!Condition::IfExitCode(1).evaluate(&ctx));

BlueprintRunner

Runs a blueprint step-by-step, managing context flow and conditions.
pub struct BlueprintRunner<'a> {
    context: StepContext,
    sandbox: &'a dyn Sandbox,
}

Methods

new()

Creates a new runner with initial context.
pub fn new(context: StepContext, sandbox: &'a dyn Sandbox) -> Self
context
StepContext
required
Initial context (working_dir, metadata)
sandbox
&'a dyn Sandbox
required
Sandbox for executing commands (local or remote)

run()

Executes the blueprint and returns the final context.
pub async fn run(mut self, blueprint: &Blueprint) -> Result<StepContext>
blueprint
&Blueprint
required
Blueprint to execute
Result<StepContext>
StepContext
Returns final context with last_output and last_exit_code from the final step
Execution Flow:
  1. For each step in sequence:
    • Evaluate condition against current context
    • If condition fails, skip step
    • If condition passes, execute step
    • On success, update context with new output/exit code
    • On error:
      • If continue_on_error: true, log warning and continue
      • If continue_on_error: false, return error immediately
  2. Return final context
Example:
use magpie_core::{
    Blueprint, BlueprintRunner, Step, StepKind, ShellStep, AgentStep,
    Condition, StepContext, LocalSandbox,
};
use std::path::PathBuf;

let bp = Blueprint::new("demo")
    .add_step(Step {
        name: "list-files".to_string(),
        kind: StepKind::Shell(ShellStep::new("ls").with_args(vec!["-la".to_string()])),
        condition: Condition::Always,
        continue_on_error: false,
    })
    .add_step(Step {
        name: "summarize".to_string(),
        kind: StepKind::Agent(
            AgentStep::new("Summarize the file listing")
                .with_last_output()
        ),
        condition: Condition::IfExitCode(0),
        continue_on_error: false,
    });

let working_dir = PathBuf::from("/workspace");
let sandbox = LocalSandbox::from_path(working_dir.clone());
let runner = BlueprintRunner::new(StepContext::new(working_dir), &sandbox);

let ctx = runner.run(&bp).await?;
println!("Final output: {:?}", ctx.last_output);

Step Types

ShellStep

A deterministic shell step — runs a command and captures output + exit code.
pub struct ShellStep {
    pub command: String,
    pub args: Vec<String>,
}

Methods

new()
Creates a new shell step.
pub fn new(command: impl Into<String>) -> Self
command
impl Into<String>
required
Command to execute (e.g. “ls”, “cargo”, “git”)
with_args()
Sets command arguments (builder pattern).
pub fn with_args(mut self, args: Vec<String>) -> Self
args
Vec<String>
required
Command arguments
execute()
Executes the command via sandbox.
pub async fn execute(&self, ctx: &StepContext, sandbox: &dyn Sandbox) -> Result<StepContext>
Behavior:
  • Executes command via sandbox.exec()
  • Combines stdout + stderr into last_output
  • Sets last_exit_code
  • Returns updated context
Example:
use magpie_core::{ShellStep, StepContext, LocalSandbox};
use std::path::PathBuf;

let step = ShellStep::new("echo").with_args(vec!["hello".to_string()]);
let ctx = StepContext::new(PathBuf::from("/tmp"));
let sandbox = LocalSandbox::from_path(PathBuf::from("/tmp"));

let result = step.execute(&ctx, &sandbox).await?;
assert_eq!(result.last_exit_code, Some(0));
assert!(result.last_output.unwrap().contains("hello"));

AgentStep

An agentic step — runs MagpieAgent with a prompt, optionally injecting prior output.
pub struct AgentStep {
    pub prompt: String,
    pub max_turns: Option<u32>,
    pub include_last_output: bool,
    pub context_metadata_key: Option<String>,
    pub step_name: Option<String>,
}

Methods

new()
Creates a new agent step.
pub fn new(prompt: impl Into<String>) -> Self
prompt
impl Into<String>
required
Natural language prompt for the agent
with_max_turns()
Sets custom max turns (builder pattern).
pub fn with_max_turns(mut self, turns: u32) -> Self
turns
u32
required
Maximum conversation turns
with_last_output()
Injects previous step’s output into the prompt (builder pattern).
pub fn with_last_output(mut self) -> Self
Prepends previous step output to the prompt:
Previous step output:
last_output

Original prompt:
with_context_from_metadata()
Injects a metadata value into the prompt (builder pattern).
pub fn with_context_from_metadata(mut self, key: impl Into<String>) -> Self
key
impl Into<String>
required
Metadata key to inject (e.g. “chat_history”, “trace_dir”)
Prepends metadata value to the prompt:
Context from conversation:
metadata_value

Original prompt:
execute()
Executes the agent step.
pub async fn execute(
    &self,
    ctx: &StepContext,
    step_name: &str,
    sandbox: &dyn Sandbox,
) -> Result<StepContext>
Behavior:
  • For local sandboxes: Uses MagpieAgent with full Goose agent loop
  • For Daytona sandboxes: Execs claude -p inside the sandbox remotely
  • Injects context_metadata_key if set
  • Injects last_output if include_last_output: true
  • Sets last_output to agent response
  • Sets last_exit_code to 0 (agents don’t have exit codes)
  • Returns updated context
Example:
use magpie_core::{AgentStep, StepContext, LocalSandbox};
use std::path::PathBuf;

let step = AgentStep::new("Add a health check endpoint")
    .with_max_turns(10)
    .with_context_from_metadata("chat_history");

let mut ctx = StepContext::new(PathBuf::from("/workspace"));
ctx.metadata.insert("chat_history".to_string(), "Previous messages...".to_string());

let sandbox = LocalSandbox::from_path(PathBuf::from("/workspace"));
let result = step.execute(&ctx, "implement", &sandbox).await?;

println!("Agent response: {}", result.last_output.unwrap());

Complete Example

use magpie_core::{
    Blueprint, BlueprintRunner, Step, StepKind, ShellStep, AgentStep,
    Condition, StepContext, LocalSandbox,
};
use std::path::PathBuf;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let working_dir = PathBuf::from("/workspace/my-repo");

    // Build a TDD-style blueprint
    let bp = Blueprint::new("mini-tdd")
        // Step 1: Scan repo
        .add_step(Step {
            name: "scan-repo".to_string(),
            kind: StepKind::Shell(
                ShellStep::new("find")
                    .with_args(vec![
                        ".".to_string(),
                        "-type".to_string(),
                        "f".to_string(),
                        "-name".to_string(),
                        "*.rs".to_string(),
                    ])
            ),
            condition: Condition::Always,
            continue_on_error: false,
        })
        // Step 2: Write tests
        .add_step(Step {
            name: "write-tests".to_string(),
            kind: StepKind::Agent(
                AgentStep::new("Write unit tests for the main module")
                    .with_last_output()
                    .with_max_turns(10)
            ),
            condition: Condition::IfExitCode(0),
            continue_on_error: false,
        })
        // Step 3: Run tests (expect failure)
        .add_step(Step {
            name: "run-tests".to_string(),
            kind: StepKind::Shell(
                ShellStep::new("cargo").with_args(vec!["test".to_string()])
            ),
            condition: Condition::Always,
            continue_on_error: true, // TDD: tests should fail
        })
        // Step 4: Implement
        .add_step(Step {
            name: "implement".to_string(),
            kind: StepKind::Agent(
                AgentStep::new("Implement the code to make tests pass")
                    .with_last_output()
            ),
            condition: Condition::Always,
            continue_on_error: false,
        })
        // Step 5: Run tests again (expect success)
        .add_step(Step {
            name: "verify".to_string(),
            kind: StepKind::Shell(
                ShellStep::new("cargo").with_args(vec!["test".to_string()])
            ),
            condition: Condition::Always,
            continue_on_error: false,
        });

    // Execute blueprint
    let sandbox = LocalSandbox::from_path(working_dir.clone());
    let runner = BlueprintRunner::new(StepContext::new(working_dir), &sandbox);

    let ctx = runner.run(&bp).await?;

    println!("Blueprint completed!");
    println!("Final exit code: {:?}", ctx.last_exit_code);
    println!("Final output: {:?}", ctx.last_output);

    Ok(())
}

Error Handling

The runner stops execution when a step fails, unless continue_on_error: true. Example:
let bp = Blueprint::new("fault-tolerant")
    .add_step(Step {
        name: "might-fail".to_string(),
        kind: StepKind::Shell(ShellStep::new("false")),
        condition: Condition::Always,
        continue_on_error: true, // Pipeline continues even if this fails
    })
    .add_step(Step {
        name: "always-runs".to_string(),
        kind: StepKind::Shell(ShellStep::new("echo").with_args(vec!["recovered".to_string()])),
        condition: Condition::Always,
        continue_on_error: false,
    });

let ctx = runner.run(&bp).await?;
assert!(ctx.last_output.unwrap().contains("recovered"));

Built-in Blueprints

Magpie includes three built-in blueprints in pipeline.rs:
  • Simple (build_main_blueprint): Single agent call for docs/typos
  • TDD (build_tdd_blueprint): 7-step TDD flow for features
  • Diagnostic (build_diagnostic_blueprint): 8-step investigation flow for bugs
See Pipeline API for details.

Build docs developers (and LLMs) love