Skip to main content

Engine

Struct Engine 

Source
pub struct Engine { /* private fields */ }
Expand description

The workflow orchestration engine.

Holds references to the store, agent provider, and a registry of WorkflowHandlers.

§Examples

use std::sync::Arc;
use ironflow_engine::engine::Engine;
use ironflow_engine::config::ShellConfig;
use ironflow_engine::handler::{WorkflowHandler, HandlerFuture, WorkflowInfo};
use ironflow_engine::context::WorkflowContext;
use ironflow_store::memory::InMemoryStore;
use ironflow_store::models::TriggerKind;
use ironflow_core::providers::claude::ClaudeCodeProvider;
use serde_json::json;

struct CiWorkflow;
impl WorkflowHandler for CiWorkflow {
    fn name(&self) -> &str { "ci" }
    fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
        Box::pin(async move {
            ctx.shell("test", ShellConfig::new("cargo test")).await?;
            Ok(())
        })
    }
}

let store = Arc::new(InMemoryStore::new());
let provider = Arc::new(ClaudeCodeProvider::new());
let mut engine = Engine::new(store, provider);
engine.register(CiWorkflow)?;

let result = engine.run_handler("ci", TriggerKind::Manual, json!({})).await?;
tracing::info!(run_id = %result.run.id, status = ?result.run.status, steps = result.steps.len(), "run completed");

Implementations§

Source§

impl Engine

Source

pub fn new(store: Arc<dyn Store>, provider: Arc<dyn AgentProvider>) -> Self

Create a new engine with the given store and agent provider.

§Examples
use std::sync::Arc;
use ironflow_engine::engine::Engine;
use ironflow_store::memory::InMemoryStore;
use ironflow_core::providers::claude::ClaudeCodeProvider;

let engine = Engine::new(
    Arc::new(InMemoryStore::new()),
    Arc::new(ClaudeCodeProvider::new()),
);
Source

pub fn with_budget_config(self, budget: BudgetConfig) -> Self

Apply cost guardrails to this engine.

Without this, both the per-run cap default and the monthly quota are disabled and the engine behaves exactly as before.

§Examples
use std::sync::Arc;
use ironflow_core::providers::claude::ClaudeCodeProvider;
use ironflow_engine::budget::BudgetConfig;
use ironflow_engine::engine::Engine;
use ironflow_store::memory::InMemoryStore;

let engine = Engine::new(
    Arc::new(InMemoryStore::new()),
    Arc::new(ClaudeCodeProvider::new()),
)
.with_budget_config(BudgetConfig::from_env());
Source

pub fn budget_config(&self) -> &BudgetConfig

Returns the cost guardrails applied by this engine.

Source

pub fn set_log_sender(&mut self, sender: LogSender)

Attach a log sender for real-time step output streaming.

When set, all workflow contexts created by this engine will forward step output (shell stdout/stderr, agent system messages) to the given sender.

Source

pub fn set_artifact_sink(&mut self, sink: Arc<dyn ArtifactSink>)

Attach the backend that stores and serves artifact bytes.

Every context this engine builds inherits it. Without one, steps that declare artifacts fail explicitly and every other step is unaffected.

§Examples
use std::sync::Arc;

use ironflow_engine::artifact::ArtifactSink;
use ironflow_engine::engine::Engine;

engine.set_artifact_sink(sink);
Source

pub fn artifact_sink(&self) -> Option<&Arc<dyn ArtifactSink>>

The artifact backend attached to this engine, if any.

Source

pub fn set_event_bus(&mut self, bus: WorkflowEventBus)

Attach a WorkflowEventBus for per-run real-time monitoring.

When set, all workflow contexts created by this engine will publish step-level events (StepStarted, StepCompleted, StepFailed) to the bus.

§Examples
use ironflow_engine::engine::Engine;
use ironflow_engine::notify::WorkflowEventBus;

engine.set_event_bus(WorkflowEventBus::new());
Source

pub fn event_bus(&self) -> Option<&WorkflowEventBus>

The workflow event bus attached to this engine, if any.

Source

pub fn store(&self) -> &Arc<dyn Store>

Returns a reference to the backing store.

Source

pub fn provider(&self) -> &Arc<dyn AgentProvider>

Returns a reference to the agent provider.

Source

pub fn register( &mut self, handler: impl WorkflowHandler + 'static, ) -> Result<(), EngineError>

Register a WorkflowHandler for dynamic workflow execution.

The handler is looked up by WorkflowHandler::name when executing or enqueuing.

§Errors

Returns EngineError::InvalidWorkflow if a handler with the same name is already registered.

§Examples
use std::sync::Arc;
use ironflow_engine::engine::Engine;
use ironflow_engine::handler::{WorkflowHandler, HandlerFuture};
use ironflow_engine::context::WorkflowContext;
use ironflow_engine::config::ShellConfig;
use ironflow_store::memory::InMemoryStore;
use ironflow_core::providers::claude::ClaudeCodeProvider;

struct MyWorkflow;
impl WorkflowHandler for MyWorkflow {
    fn name(&self) -> &str { "my-workflow" }
    fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
        Box::pin(async move {
            ctx.shell("step1", ShellConfig::new("echo done")).await?;
            Ok(())
        })
    }
}

let mut engine = Engine::new(
    Arc::new(InMemoryStore::new()),
    Arc::new(ClaudeCodeProvider::new()),
);
engine.register(MyWorkflow)?;
Source

pub fn register_boxed( &mut self, handler: Box<dyn WorkflowHandler>, ) -> Result<(), EngineError>

Register a pre-boxed workflow handler.

§Errors

Returns EngineError::InvalidWorkflow if a handler with the same name is already registered or if its category is invalid.

Source

pub fn get_handler(&self, name: &str) -> Option<&Arc<dyn WorkflowHandler>>

Get a registered handler by name.

Source

pub fn handler_names(&self) -> Vec<&str>

List registered handler names.

Source

pub fn handler_info(&self, name: &str) -> Option<WorkflowInfo>

Get detailed info about a registered workflow handler.

Source

pub fn scheduled_handlers(&self) -> Vec<(&str, &CronSchedule)>

List handlers that have a cron schedule configured.

Returns pairs of (workflow_name, cron_expression) for all handlers where WorkflowHandler::schedule returns Some.

Use this to wire scheduled handlers into a cron scheduler (e.g. ironflow_runtime::Runtime::cron).

§Examples
let engine = Engine::new(
    Arc::new(InMemoryStore::new()),
    Arc::new(ClaudeCodeProvider::new()),
);
for (name, schedule) in engine.scheduled_handlers() {
    tracing::info!("{name} runs on schedule: {schedule}");
}
Source

pub fn subscribe( &mut self, subscriber: impl EventSubscriber + 'static, event_types: &[&'static str], )

Register an event subscriber for domain events.

The subscriber is called only for events whose type is in event_types. Pass Event::ALL to receive every event.

§Examples
use ironflow_engine::engine::Engine;
use ironflow_engine::notify::{Event, WebhookSubscriber};
use ironflow_store::memory::InMemoryStore;
use ironflow_core::providers::claude::ClaudeCodeProvider;
use std::sync::Arc;

let mut engine = Engine::new(
    Arc::new(InMemoryStore::new()),
    Arc::new(ClaudeCodeProvider::new()),
);

engine.subscribe(
    WebhookSubscriber::new("https://hooks.example.com/events"),
    &[Event::RUN_STATUS_CHANGED, Event::STEP_FAILED],
);
Source

pub fn event_publisher(&self) -> &EventPublisher

Returns a reference to the event publisher.

Useful for publishing events from outside the engine (e.g. auth routes in the API layer).

Source

pub async fn run_handler( &self, handler_name: &str, trigger: TriggerKind, payload: Value, ) -> Result<WorkflowResult, EngineError>

Execute a registered handler inline.

Creates a run, builds a WorkflowContext, calls the handler’s execute, and finalizes the run.

§Errors

Returns EngineError::InvalidWorkflow if no handler is registered with that name. Returns EngineError if execution fails.

§Examples
use std::sync::Arc;
use ironflow_engine::engine::Engine;
use ironflow_store::memory::InMemoryStore;
use ironflow_store::models::TriggerKind;
use ironflow_core::providers::claude::ClaudeCodeProvider;
use serde_json::json;

let run = engine.run_handler("deploy", TriggerKind::Manual, json!({})).await?;
Source

pub async fn enqueue_handler( &self, handler_name: &str, trigger: TriggerKind, payload: Value, max_retries: u32, ) -> Result<Run, EngineError>

Enqueue a handler-based workflow for worker execution.

The workflow name is stored in the run. The worker looks up the handler by name when executing.

§Errors

Returns EngineError::InvalidWorkflow if no handler is registered. Returns EngineError::MonthlyBudgetExceeded if the monthly cost quota is exhausted.

Source

pub async fn enqueue_handler_with_options( &self, handler_name: &str, trigger: TriggerKind, payload: Value, options: EnqueueOptions, ) -> Result<RunCreation, EngineError>

Enqueue a handler-based workflow with labels, deferred scheduling, an optional cost cap, an optional author, and an optional idempotency key.

See EnqueueOptions for the individual settings.

When EnqueueOptions::idempotency_key is set and already bound to a run created within IDEMPOTENCY_WINDOW, nothing is enqueued and the original run is returned as RunCreation::Existing.

§Errors

Returns EngineError::InvalidWorkflow if no handler is registered. Returns EngineError::MonthlyBudgetExceeded if the monthly cost quota is exhausted. Returns EngineError::Store if the run cannot be persisted.

§Examples
use ironflow_engine::engine::{Engine, EnqueueOptions};
use ironflow_store::models::TriggerKind;
use serde_json::json;

let creation = engine
    .enqueue_handler_with_options(
        "deploy",
        TriggerKind::Api,
        json!({"env": "prod"}),
        EnqueueOptions {
            max_retries: 3,
            idempotency_key: Some("github:abc-123".to_string()),
            ..Default::default()
        },
    )
    .await?;

if creation.is_created() {
    println!("enqueued {}", creation.run().id);
}
Source

pub async fn execute_handler_run( &self, run_id: Uuid, ) -> Result<WorkflowResult, EngineError>

Execute a handler-based run (used by the worker after pick_next_pending).

Looks up the handler by the run’s workflow_name and executes it with a fresh WorkflowContext.

§Errors

Returns EngineError::InvalidWorkflow if no handler matches.

Source

pub async fn execute_run( &self, run_id: Uuid, ) -> Result<WorkflowResult, EngineError>

Execute a run by its ID (used by the worker after pick_next_pending).

Delegates to execute_handler_run.

§Errors

Returns EngineError if the run is not found or execution fails.

Source

pub async fn resume_run( &self, run_id: Uuid, ) -> Result<WorkflowResult, EngineError>

Resume a run after human approval.

Re-executes the handler with step replay: completed steps return cached output, approved approval steps are skipped, and execution continues from the first unexecuted step.

Supports multiple approval gates – each resume replays all prior steps and stops at the next approval (or completes the run).

§Errors

Returns EngineError::InvalidWorkflow if no handler matches. Returns EngineError if execution fails or hits another approval.

Source

pub async fn fail_or_schedule_retry( &self, run_id: Uuid, error: &str, retryable: bool, cost_usd: Option<Decimal>, duration_ms: Option<u64>, ) -> Result<RunStatus, EngineError>

Record a run failure, replaying the run later when retries remain.

This is the single place where a failed run’s fate is decided. When retryable is true and the run has not exhausted max_retries, the run moves to RunStatus::Retrying with scheduled_at set to now + backoffpick_next_pending picks it up again once that time has passed. Otherwise the run moves to RunStatus::Failed.

Either way, steps left non-terminal by the failed attempt are closed via fail_orphaned_steps so they are never confused with the next attempt’s steps.

Callers pass retryable explicitly rather than an error value, because the worker classifies failures it observes from the outside (a timeout, a panicked task) that never produce an EngineError. Use is_run_retryable to classify an EngineError.

cost_usd and duration_ms are None when the caller does not know the totals (a timeout or a panic observed from outside the handler); the values already stored on the run are then left untouched.

Returns the status the run was moved to.

§Errors

Returns EngineError::Store if the run does not exist or the update cannot be persisted.

§Examples
use ironflow_engine::engine::Engine;
use ironflow_engine::error::EngineError;
use uuid::Uuid;

let status = engine
    .fail_or_schedule_retry(run_id, "run timed out after 300s", true, None, None)
    .await?;
Source

pub async fn fail_orphaned_steps( &self, run_id: Uuid, error_message: &str, ) -> Result<(), EngineError>

Fail all non-terminal steps for a run.

Called after a run is marked as failed (timeout, error, panic) to clean up orphaned steps that are still in Running, Pending, or AwaitingApproval.

  • Running / AwaitingApproval steps are marked Failed.
  • Pending steps are marked Skipped (FSM does not allow Pending -> Failed).

Errors from individual step updates are logged but do not abort the cleanup.

§Errors

Returns EngineError if listing steps fails.

Trait Implementations§

Source§

impl Debug for Engine

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more