| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A Rust-based event-driven workflow engine with real-time execution monitoring. Designed for sandboxed, parallel DAG execution with Firecracker microVMs.
FlowEngine focuses on speed, sandboxing, and streaming. Here's how it stacks up against familiar tools:
| FlowEngine | Prefect | Airflow | |
|---|---|---|---|
| Runtime | Rust (single binary) | Python | Python |
| Sandbox | Firecracker μVMs | Docker | Docker/K8s |
| Streaming | Native WebSocket | Polling | Log files |
| Latency | <10ms per node | 100–500ms | 500ms+ |
| Python API | @task, Flow, Sandbox | @task, @flow | @task, DAG |
| Retry | Exponential backoff | Exponential backoff | Linear |
| Persistence | SQLite (built-in) | Postgres | Postgres |
| Deployment | flow + flowserver binaries | Server + workers | Scheduler + workers |
flowengine/ ├── flowcore - Core abstractions (Node trait, Value type, Events, RetryPolicy) ├── flowcore_macros - Derive macros (`#[derive(NodeConfig)]`) ├── flowruntime - Execution engine (DAG executor, Registry, Runtime) ├── flownodes - Standard node library (shell, zypi, docker, http, transform, debug) ├── flowpersist - SQLite-backed persistence & result caching ├── flowserver - HTTP/WebSocket API server (Actix-based) ├── flowcli - Command-line interface └── python/ - Python SDK (flowengine package)
cargo build --release./target/release/flow run \
--file examples/shell_pipeline.yaml \
--verbose# Start Zypi first
cd ../../exs/zypi && docker compose up -d
# Run sandboxed
./target/release/flow run \
--file examples/zypi_sandbox.json \
--input '{"value": 42, "text": "hello from firecracker"}' \
--verbose./target/release/flowserver
# → http://localhost:3000
# → WebSocket: ws://localhost:3000/api/eventsSee Documentation Index for all guides and references.
pip install -e python/from flowengine import Flow, task
import requests
@task(retry=3, timeout=30)
def fetch_data(url: str) -> dict:
return requests.get(url).json()
@task()
def process(data: dict) -> dict:
return {"summary": data["title"]}
flow = Flow("data-pipeline")
flow >> fetch_data >> process
result = flow.run(url="https://api.github.com/zen")from flowengine import Sandbox
sandbox = Sandbox(image="ubuntu:24.04")
# Execute commands in Firecracker microVMs
exit_code, stdout, stderr = sandbox.exec(["python3", "script.py"])
# File injection
result = sandbox.exec(
["python3", "/app/analyze.py"],
files={"/app/analyze.py": "print('sandboxed!')"},
)
# check_output — raises on failure
output = sandbox.check_output(["echo", "hello"])
# Context manager
with Sandbox() as s:
s.exec(["ls", "-la"]){
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example Workflow",
"nodes": [
{
"id": "node-1",
"node_type": "http.request",
"name": "Fetch Data",
"config": {
"method": {"type": "String", "value": "GET"}
},
"retry_policy": {
"max_attempts": 3,
"delay_ms": 1000,
"backoff_multiplier": 2.0,
"max_delay_ms": 60000,
"retry_on_timeout": true
}
},
{
"id": "node-2",
"node_type": "debug.log",
"name": "Log Result"
}
],
"connections": [
{
"from_node": "node-1",
"from_port": "body",
"to_node": "node-2",
"to_port": "message"
}
],
"settings": {
"max_parallel_nodes": 10,
"on_error": "StopWorkflow"
}
}from flowengine import FlowBuilder, task
builder = FlowBuilder("explicit-pipeline")
fetch = builder.add(fetch_data)
proc = builder.add(process)
builder.connect(fetch, "output", proc, "input")
flow = builder.build()
flow.save("workflow.json")shell.exec — Execute local commands
zypi.exec — Execute in Firecracker microVM via Zypi API
transform.json_parse — Parse JSON strings
transform.json_stringify — Convert values to JSON
time.delay — Delay execution (passthrough inputs)
debug.log — Log values for debugging
Every node supports exponential backoff retry:
{
"retry_policy": {
"max_attempts": 5,
"delay_ms": 1000,
"backoff_multiplier": 2.0,
"max_delay_ms": 60000,
"retry_on_timeout": true
}
}Delays: 1s → 2s → 4s → 8s → 16s (capped at 60s max).
Nodes emit real-time events streamed to CLI, WebSocket, or programmatic subscribers:
let mut events = runtime.subscribe_events();
while let Ok(event) = events.recv().await {
match event {
ExecutionEvent::NodeStarted { node_id, node_type, .. } => { }
ExecutionEvent::NodeCompleted { node_id, duration_ms, .. } => { }
ExecutionEvent::NodeFailed { node_id, error, .. } => { }
ExecutionEvent::NodeEvent { event, .. } => match event {
NodeEvent::Info { message } => { }
NodeEvent::Warning { message } => { }
NodeEvent::Progress { percent, message } => { }
NodeEvent::StdoutLine { line } => { } // streaming!
NodeEvent::StderrLine { line } => { } // streaming!
_ => { }
}
_ => { }
}
}use flowpersist::PersistentStore;
let store = PersistentStore::open("flowengine.db")?;
// Save a workflow
store.save_workflow(&workflow)?;
// Load it back
let wf = store.load_workflow(id)?;
// Record execution history
store.record_execution(&ExecutionRecord { ... })?;
// Cache node results with content fingerprint
let config_hash = PersistentStore::compute_hash(&config);
let input_hash = PersistentStore::compute_hash(&inputs);
store.cache_result("shell.exec", &config_hash, &input_hash, &outputs, Some(3600))?;
// Check cache before re-executing
if let Some(cached) = store.get_cached_result("shell.exec", &config_hash, &input_hash)? {
return Ok(cached); // cache hit!
}use async_trait::async_trait;
use flowcore::{Node, NodeContext, NodeError, NodeOutput, Value};
pub struct MyCustomNode;
#[async_trait]
impl Node for MyCustomNode {
fn node_type(&self) -> &str { "custom.my_node" }
async fn execute(&self, ctx: NodeContext) -> Result<NodeOutput, NodeError> {
let input = ctx.require_input("data")?;
ctx.events.info("Processing...");
ctx.events.progress(50.0, Some("Halfway".to_string()));
// Stream output to subscribers
ctx.events.stdout_line("processing item 1");
ctx.events.stdout_line("processing item 2");
Ok(NodeOutput::new()
.with_output("result", "done"))
}
}use flowruntime::{NodeFactory, NodeMetadata, PortDefinition};
pub struct MyCustomNodeFactory;
impl NodeFactory for MyCustomNodeFactory {
fn create(&self, _config: &HashMap<String, Value>) -> Result<Box<dyn Node>, NodeError> {
Ok(Box::new(MyCustomNode))
}
fn node_type(&self) -> &str { "custom.my_node" }
fn metadata(&self) -> NodeMetadata {
NodeMetadata {
description: "My custom node".to_string(),
category: "custom".to_string(),
inputs: vec![PortDefinition {
name: "data".to_string(),
description: "Input data".to_string(),
required: true,
}],
outputs: vec![PortDefinition {
name: "result".to_string(),
description: "Processed result".to_string(),
required: false,
}],
}
}
}registry.register(Arc::new(MyCustomNodeFactory));# Run a workflow
flow run --file workflow.json --input '{"key": "value"}' --verbose
# Validate workflow
flow validate workflow.json
# List available node types
flow nodes
# Create example workflow
flow init --output my_workflow.jsonContributions welcome! Priority areas:
MIT OR Apache-2.0
Inspired by excellent workflow tools that came before:
| Back | FazBrowse Home | New Git URL |