4 stable releases
1.1.0 | Feb 27, 2025 |
---|---|
1.0.12 | Feb 27, 2025 |
#87 in Science
388 downloads per month
280KB
2.5K
SLoC
π Floxide: The Power of Workflows in Rust
A type-safe, composable directed graph workflow system written in Rust.
π« Overview
Floxide transforms complex workflow orchestration into a delightful experience. Built with Rust's powerful type system at its core, Floxide provides a flexible, performant, and type-safe way to create sophisticated workflow graphs with crystal-clear transitions between steps.
β¨ Key Features
- π Type-Safe By Design: Leverage Rust's type system for compile-time workflow correctness
- 𧩠Composable Architecture: Build complex workflows from simple, reusable components
- β‘ Async First: Native support for asynchronous execution with Tokio
- π Advanced Patterns: Support for batch processing, event-driven workflows, and more
- πΎ State Management: Built-in serialization for workflow persistence
- π Observability: Comprehensive tracing and monitoring capabilities
- π§ͺ Testable: Design your workflows for easy testing and verification
π Architectural Decisions
This project follows documented architectural decisions recorded in ADRs (Architectural Decision Records). Each ADR captures the context, decision, and consequences of significant architectural choices. The development is guided by an LLM with rules defined in the .cursorrules
file.
Key architectural decisions include:
-
Core Framework Abstractions - Defining the fundamental abstractions like Node, Action, and Workflow with a trait-based approach for type safety and flexibility.
-
Project Structure and Crate Organization - Organizing the framework as a Cargo workspace with multiple specialized crates for modularity and separation of concerns.
-
Async Runtime Selection - Choosing Tokio as the primary async runtime for its comprehensive feature set and wide adoption.
-
Node Lifecycle Methods - Implementing a three-phase lifecycle (prep/exec/post) for workflow nodes to provide clear separation of concerns.
-
Batch Processing Implementation - Designing a batch processing system that efficiently handles parallel execution with configurable concurrency limits.
-
Event-Driven Workflow Pattern - Extending the framework with event-driven capabilities for handling asynchronous events.
-
Reactive Node Implementation - Creating nodes that can respond to changes in external data sources using a stream-based approach.
-
Timer Node Implementation - Supporting time-based scheduling for workflow execution with various scheduling patterns.
-
Long-Running Node Implementation - Enabling workflows to process work incrementally with state persistence between executions.
-
Simplified Publishing with Maintained Subcrate Structure - Using cargo-workspaces for version management and publishing.
-
Script Consolidation for Release Process - Streamlining the release process with consolidated scripts.
π¦ Release Process
Floxide uses cargo-workspaces for version management and publishing. The release process is automated through a GitHub Actions workflow:
Combined Release Workflow: The combined-release.yml
workflow handles the entire release process in one go. It can be triggered manually from the GitHub Actions tab with options for:
- Bump type (patch, minor, major)
- Dry run (preview without making changes)
- Whether to publish to crates.io
- Tagging options
This workflow combines version bumping, tagging, and publishing into a single, streamlined process.
For local development and testing, you can use the following scripts:
./scripts/release_with_workspaces.sh <version> [--dry-run] [--skip-publish]
- Bump version and optionally publish./scripts/update_versions.sh
- Update subcrate versions to use workspace inheritance./scripts/run_ci_locally.sh
- Run CI checks locally./scripts/serve-docs.sh
- Serve documentation locally
For more details on the release process, see ADR-0035: Combined Version Bump and Release Workflow.
π Quick Start
Add Floxide to your project:
[dependencies]
floxide = { version = "1.0.0", features = ["transform", "event"] }
Create your first workflow:
use floxide::{lifecycle_node, LifecycleNode, Workflow, DefaultAction, FloxideError};
use async_trait::async_trait;
use std::sync::Arc;
// Define your context type
#[derive(Debug, Clone)]
struct MessageContext {
input: String,
result: Option<String>,
}
// Create a node using the convenience function
fn create_processor_node() -> impl LifecycleNode<MessageContext, DefaultAction> {
lifecycle_node(
Some("processor"), // Node ID
|ctx: &mut MessageContext| async move {
// Preparation phase
println!("Preparing to process: {}", ctx.input);
Ok(ctx.input.clone())
},
|input: String| async move {
// Execution phase
println!("Processing message...");
Ok(format!("β
Processed: {}", input))
},
|_prep, exec_result, ctx: &mut MessageContext| async move {
// Post-processing phase
ctx.result = Some(exec_result);
Ok(DefaultAction::Next)
},
)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a context
let mut context = MessageContext {
input: "Hello, Floxide!".to_string(),
result: None,
};
// Create a node and workflow
let node = Arc::new(create_processor_node());
let mut workflow = Workflow::new(node);
// Execute the workflow
workflow.execute(&mut context).await?;
// Print the result
println!("Result: {:?}", context.result);
Ok(())
}
π¦ Feature Flags
Floxide uses feature flags to allow you to include only the functionality you need:
Feature | Description | Dependencies |
---|---|---|
core |
Core abstractions and functionality (default) | None |
transform |
Transform node implementations | core |
event |
Event-driven workflow functionality | core |
timer |
Time-based workflow functionality | core |
longrunning |
Long-running process functionality | core |
reactive |
Reactive workflow functionality | core |
full |
All features | All of the above |
Example of using specific features:
# Only include core and transform functionality
floxide = { version = "1.0.0", features = ["transform"] }
# Include event-driven and timer functionality
floxide = { version = "1.0.0", features = ["event", "timer"] }
# Include all functionality
floxide = { version = "1.0.0", features = ["full"] }
𧩠Workflow Pattern Examples
Floxide supports a wide variety of workflow patterns through its modular crate system. Each pattern is designed to solve specific workflow challenges:
π Simple Chain (Linear Workflow)
A basic sequence of nodes executed one after another. This is the foundation of all workflows.
graph LR
A["Process Data"] --> B["Format Output"] --> C["Store Result"]
style A fill:#c4e6ff,stroke:#1a73e8,stroke-width:2px,color:black
style B fill:#c4e6ff,stroke:#1a73e8,stroke-width:2px,color:black
style C fill:#c4e6ff,stroke:#1a73e8,stroke-width:2px,color:black
Example: lifecycle_node.rs
π² Conditional Branching
Workflows that make decisions based on context data or node results, directing flow through different paths.
graph TD
A["Validate Input"] -->|Valid| B["Process Data"]
A -->|Invalid| C["Error Handler"]
B -->|Success| D["Format Output"]
B -->|Error| C
style A fill:#c4e6ff,stroke:#1a73e8,stroke-width:2px,color:black
style B fill:#c4e6ff,stroke:#1a73e8,stroke-width:2px,color:black
style C fill:#ffcccc,stroke:#e53935,stroke-width:2px,color:black
style D fill:#c4e6ff,stroke:#1a73e8,stroke-width:2px,color:black
Example: order_processing.rs
π Transform Pipeline
A specialized workflow for data transformation, where each node transforms input to output in a functional style.
graph LR
A["Raw Data"] --> B["Validate"] --> C["Transform"] --> D["Format"] --> E["Output"]
style A fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:black
style B fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:black
style C fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:black
style D fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:black
style E fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:black
Example: transform_node.rs
π Parallel Batch Processing
Process multiple items concurrently with controlled parallelism, ideal for high-throughput data processing.
graph TD
A["Batch Input"] --> B["Split Batch"]
B --> C1["Process Item 1"]
B --> C2["Process Item 2"]
B --> C3["Process Item 3"]
C1 --> D["Aggregate Results"]
C2 --> D
C3 --> D
style A fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:black
style B fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:black
style C1 fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:black
style C2 fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:black
style C3 fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:black
style D fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:black
Example: batch_processing.rs
π‘ Event-Driven Flow
Workflows that respond to external events, ideal for building reactive systems that process events as they arrive.
graph TD
A["Event Source"] -->|Events| B["Event Classifier"]
B -->|Type A| C["Handler A"]
B -->|Type B| D["Handler B"]
B -->|Type C| E["Handler C"]
C --> F["Event Source"]
D --> F
E --> F
style A fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:black
style B fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:black
style C fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:black
style D fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:black
style E fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:black
style F fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:black
Example: event_driven_workflow.rs
β±οΈ Time-Based Workflows
Workflows that execute based on time schedules, supporting one-time, interval, and calendar-based scheduling.
graph TD
A["Timer Source"] -->|Trigger| B["Scheduled Task"]
B --> C["Process Result"]
C -->|Reschedule| A
style A fill:#fff8e1,stroke:#ff8f00,stroke-width:2px,color:black
style B fill:#fff8e1,stroke:#ff8f00,stroke-width:2px,color:black
style C fill:#fff8e1,stroke:#ff8f00,stroke-width:2px,color:black
Example: timer_node.rs
π Reactive Workflows
Workflows that react to changes in external data sources, such as files, databases, or streams.
graph TD
A["Data Source"] -->|Change| B["Change Detector"]
B --> C["Process Change"]
C --> D["Update State"]
D --> A
style A fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:black
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:black
style C fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:black
style D fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:black
Example: reactive_node.rs
βΈοΈ Long-Running Processes
Workflows for processes that can be suspended and resumed, with state persistence between executions.
graph TD
A["Start Process"] --> B["Execute Step"]
B -->|Complete| C["Final Result"]
B -->|Suspend| D["Save State"]
D -->|Resume| B
style A fill:#fce4ec,stroke:#c2185b,stroke-width:2px,color:black
style B fill:#fce4ec,stroke:#c2185b,stroke-width:2px,color:black
style C fill:#fce4ec,stroke:#c2185b,stroke-width:2px,color:black
style D fill:#fce4ec,stroke:#c2185b,stroke-width:2px,color:black
Example: longrunning_node.rs
π€ Multi-Agent LLM System
A workflow pattern for orchestrating multiple AI agents that collaborate to solve complex tasks.
graph TD
A["User Input"] --> B["Router Agent"]
B -->|Research Task| C["Research Agent"]
B -->|Code Task| D["Coding Agent"]
B -->|Analysis Task| E["Analysis Agent"]
C --> F["Aggregator Agent"]
D --> F
E --> F
F --> G["Response Generator"]
G --> H["User Output"]
style A fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
style B fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
style C fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
style D fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
style E fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
style F fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
style G fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
style H fill:#e0f7fa,stroke:#00838f,stroke-width:2px,color:black
This pattern demonstrates how to build a multi-agent LLM system where specialized agents handle different aspects of a task. Each agent is implemented as a node in the workflow, with the router determining which agents to invoke based on the task requirements. The aggregator combines results from multiple agents before generating the final response.
Implementation Example:
// Define agent context
#[derive(Debug, Clone)]
struct AgentContext {
user_query: String,
agent_responses: HashMap<String, String>,
final_response: Option<String>,
}
// Create router agent node
fn create_router_agent() -> impl LifecycleNode<AgentContext, AgentAction> {
lifecycle_node(
Some("router"),
|ctx: &mut AgentContext| async move {
// Preparation: analyze the query
println!("Router analyzing query: {}", ctx.user_query);
Ok(ctx.user_query.clone())
},
|query: String| async move {
// Execution: determine which agents to invoke
let requires_research = query.contains("research") || query.contains("information");
let requires_coding = query.contains("code") || query.contains("program");
let requires_analysis = query.contains("analyze") || query.contains("evaluate");
Ok((requires_research, requires_coding, requires_analysis))
},
|_prep, (research, coding, analysis), ctx: &mut AgentContext| async move {
// Post-processing: route to appropriate agents
if research {
return Ok(AgentAction::Research);
} else if coding {
return Ok(AgentAction::Code);
} else if analysis {
return Ok(AgentAction::Analyze);
}
Ok(AgentAction::Aggregate) // Default if no specific routing
},
)
}
// Similar implementations for research_agent, coding_agent, analysis_agent, and aggregator_agent
π Examples & Documentation
Explore our extensive examples and documentation:
Try our examples directly:
git clone https://github.com/aitoroses/floxide.git
cd floxide
cargo run --example lifecycle_node
π€ Contributing
We welcome contributions of all kinds! Whether you're fixing a bug, adding a feature, or improving documentation, your help is appreciated.
See our Contributing Guidelines for more details on how to get started.
π License
Floxide is available under the MIT License - see the LICENSE file for details.
π Acknowledgments
- The Rust community for their excellent crates and support
- Our amazing contributors who help make Floxide better every day
Dependencies
~6β14MB
~157K SLoC