A high-performance, async workflow engine for executing scripts in planned sequences, written in Rust. This is a complete reimplementation and architectural improvement of the original Python-based goblin workflow engine.
Goblin Engine allows you to:
- Define scripts with configuration via TOML files
- Create execution plans that orchestrate multiple scripts
- Handle dependencies between steps automatically
- Execute workflows asynchronously with proper error handling
- Auto-discover scripts and validate configurations
| Aspect | Original Python | New Rust Implementation |
|---|---|---|
| Performance | Synchronous execution | Fully async/concurrent execution |
| Type Safety | Runtime validation | Compile-time type safety |
| Error Handling | Exception-based | Result-based with rich error types |
| Concurrency | Threading with locks | Lock-free concurrent data structures |
| Memory Management | Garbage collected | Zero-cost abstractions, no GC overhead |
| Configuration | Basic TOML parsing | Full validation with defaults |
| Testing | Limited test coverage | Comprehensive unit tests |
| Dependency Management | Basic topological sort | Advanced cycle detection |
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Engine βββββΊβ Executor βββββΊβ Script β
β (Orchestrator)β β (Runs Commands) β β (Configuration) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β β
βΌ β
βββββββββββββββββββ βββββββββββββββββββ ββββββββββΌβββββββββ
β Plan βββββΊβ Step βββββΊβ StepInput β
β (Workflow) β β (Single Task) β β (Input Types) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
pub struct Script {
pub name: String, // Unique identifier
pub command: String, // Command to execute
pub timeout: Duration, // Execution timeout
pub test_command: Option<String>, // Optional test command
pub require_test: bool, // Whether to run test before execution
pub path: PathBuf, // Working directory
}TOML Configuration (goblin.toml):
name = "example_script"
command = "deno run --allow-all main.ts"
timeout = 500 # seconds
test_command = "deno test"
require_test = falsepub struct Plan {
pub name: String, // Plan identifier
pub steps: Vec<Step>, // Ordered execution steps
}
pub struct Step {
pub name: String, // Step identifier
pub function: String, // Script to execute
pub inputs: Vec<StepInput>, // Input arguments
pub timeout: Option<Duration>, // Override timeout
}TOML Configuration (plan file):
name = "example_plan"
[[steps]]
name = "step_one"
function = "script_name"
inputs = ["default_input"]
timeout = 1000
[[steps]]
name = "step_two"
function = "another_script"
inputs = ["step_one", "literal_value"]The engine supports three types of step inputs:
-
Literal Values: Plain string values
inputs = ["hello world", "static_value"]
-
Step References: Output from previous steps
inputs = ["previous_step_name"]
-
Templates: String interpolation with step outputs
inputs = ["Processing {step1} with {step2}"]
pub enum GoblinError {
ScriptNotFound { name: String },
PlanNotFound { name: String },
ScriptExecutionFailed { script: String, message: String },
ScriptTimeout { script: String, timeout: Duration },
TestFailed { script: String },
ConfigError { message: String },
InvalidStepConfig { message: String },
CircularDependency { plan: String },
MissingDependency { step: String, dependency: String },
// ... IO and serialization errors
}- Rust 1.70+
- Cargo
git clone <repository>
cd goblin-engine
cargo build --releasecargo install --path .
# or
cargo install goblin-engine# Initialize a new project
goblin init [directory]
# List available scripts and plans
goblin scripts
goblin plans
# Execute a single script
goblin run-script <script_name> [args...]
# Execute a plan
goblin run-plan <plan_name> --input "default input"
# Validate configuration
goblin validate
# Show statistics
goblin stats
# Generate sample configuration
goblin config > goblin.toml# Directory paths
scripts_dir = "./scripts"
plans_dir = "./plans"
# Execution settings
default_timeout = 500
require_tests = false
# Global environment variables
[environment]
API_KEY = "secret_key"
[logging]
level = "info"
stdout = true
file = "./goblin.log"
timestamps = true
[execution]
max_concurrent = 4
fail_fast = true
cleanup_temp_files = trueuse goblin_engine::{Engine, EngineConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create and configure engine
let config = EngineConfig::from_file("goblin.toml")?;
let engine = Engine::new()
.with_scripts_dir(config.scripts_dir.unwrap());
// Auto-discover scripts
engine.auto_discover_scripts()?;
// Execute a plan
let context = engine.execute_plan("my_plan", Some("input".to_string())).await?;
println!("Execution completed in {:?}", context.elapsed());
for (step, result) in context.results {
println!("{}: {}", step, result);
}
Ok(())
}project/
βββ goblin.toml # Main configuration
βββ scripts/ # Script definitions
β βββ script1/
β β βββ goblin.toml # Script config
β β βββ main.py # Implementation
β β βββ test.sh # Optional test
β βββ script2/
β βββ goblin.toml
β βββ main.ts
βββ plans/ # Execution plans
βββ plan1.toml
βββ plan2.toml
Python (goblin.toml):
name = "example"
command = "python main.py"
timeout = 500
test_command = "python -m pytest"
require_test = falseRust (same format):
name = "example"
command = "python main.py"
timeout = 500
test_command = "python -m pytest"
require_test = falsePython (plan.toml):
name = "old_plan"
[[steps]]
name = "step1"
function = "hello_world" # Note: function field
inputs = ["default_input"]Rust (plan.toml):
name = "new_plan"
[[steps]]
name = "step1"
function = "hello_world" # Same format supported
inputs = ["default_input"]- Async Execution: All operations are async in Rust version
- Better Error Messages: Rich error types with context
- Type Safety: Compile-time validation of configurations
- Performance: Significantly faster execution
- Concurrent Steps: Can execute independent steps concurrently
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_plan_executionGenerate and view API documentation:
cargo doc --open#[async_trait]
pub trait Executor {
async fn execute_script(&self, script: &Script, args: &[String]) -> Result<ExecutionResult>;
async fn run_test(&self, script: &Script) -> Result<bool>;
}Implement custom executors for different environments (Docker, remote execution, etc.).
Engine::new()- Create engine with default executorEngine::with_executor()- Create with custom executorauto_discover_scripts()- Find and load scripts from directoryexecute_plan()- Execute a workflow planexecute_script()- Execute single script
Plan::from_toml_file()- Load plan from fileget_execution_order()- Resolve dependency ordervalidate()- Check for cycles and missing dependencies
The engine automatically resolves step dependencies using topological sorting:
[[steps]]
name = "fetch_data"
inputs = ["default_input"]
[[steps]]
name = "process_data"
inputs = ["fetch_data"]
[[steps]]
name = "save_results"
inputs = ["process_data", "config_value"]Execution order: fetch_data β process_data β save_results
Use previous step outputs in later steps:
[[steps]]
name = "get_user"
inputs = ["user_id"]
[[steps]]
name = "send_email"
inputs = ["Hello {get_user}, welcome!"]The engine prevents infinite loops:
# This will fail validation
[[steps]]
name = "step_a"
inputs = ["step_b"]
[[steps]]
name = "step_b"
inputs = ["step_a"]- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for new functionality
- Run
cargo testandcargo clippy - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Original Python implementation that inspired this rewrite
- Rust community for excellent async ecosystem
- Contributors and testers
Performance Note: The Rust implementation shows 5-10x performance improvements over the Python version for typical workflows, with significantly better memory usage and concurrent execution capabilities.