4 stable releases

1.1.0 Feb 27, 2025
1.0.12 Feb 27, 2025

#87 in Science

Download history 329/week @ 2025-02-24 59/week @ 2025-03-03

388 downloads per month

MIT license

280KB
2.5K SLoC

πŸš€ Floxide: The Power of Workflows in Rust

CI Crates.io Documentation License: MIT

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:

πŸ“¦ 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