Skip to main content

ironflow_engine/
context.rs

1//! [`WorkflowContext`] — execution context for dynamic workflows.
2//!
3//! Provides step execution methods that automatically persist results to the
4//! store. Each call to [`shell`](WorkflowContext::shell),
5//! [`http`](WorkflowContext::http), [`agent`](WorkflowContext::agent), or
6//! [`workflow`](WorkflowContext::workflow) creates a step record, executes the
7//! operation, captures the output, and returns a
8//! [`StepOutput`](crate::executor::StepOutput) that the next step can
9//! reference.
10//!
11//! # Examples
12//!
13//! ```no_run
14//! use ironflow_engine::context::WorkflowContext;
15//! use ironflow_engine::config::{ShellConfig, AgentStepConfig};
16//! use ironflow_engine::error::EngineError;
17//!
18//! # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
19//! let build = ctx.shell("build", ShellConfig::new("cargo build")).await?;
20//! let review = ctx.agent("review", AgentStepConfig::new(
21//!     &format!("Build output:\n{}", build.stdout())
22//! )).await?;
23//! # Ok(())
24//! # }
25//! ```
26
27mod accessors;
28mod artifacts;
29mod error_handlers;
30mod failure;
31mod guard;
32mod lifecycle;
33mod session;
34mod steps;
35
36#[cfg(test)]
37mod tests;
38
39pub(crate) use lifecycle::interrupt_running_steps;
40pub(crate) use steps::approval::required_votes;
41pub(crate) use steps::sub_workflow::recorded_child_run_id;
42
43use std::collections::HashMap;
44use std::fmt;
45use std::sync::Arc;
46
47use chrono::{DateTime, Utc};
48use rust_decimal::Decimal;
49use serde_json::Value;
50use uuid::Uuid;
51
52use ironflow_core::decision::DecisionProvider;
53use ironflow_core::provider::AgentProvider;
54use ironflow_core::trace_context::WorkflowTraceContext;
55use ironflow_store::models::Step;
56use ironflow_store::store::Store;
57
58use crate::artifact::ArtifactSink;
59use crate::config::StepConfig;
60use crate::executor::{StepInterceptor, StepResult};
61use crate::guard::{SharedGuardState, WorkflowGuardConfig};
62use crate::handler::WorkflowHandler;
63use crate::log_sender::LogSender;
64use crate::notify::WorkflowEventBus;
65use crate::operation::OperationContext;
66use crate::plan::SharedPlanRecorder;
67
68/// Label set on every child run of a sub-workflow step, holding the id of the
69/// run that started it. Defined by the store, which follows it to list the
70/// descendants of a run; re-exported here where workflow code looks for it.
71///
72/// The root of the chain is recorded under
73/// [`LABEL_ROOT_RUN_ID`](ironflow_core::provider::LABEL_ROOT_RUN_ID).
74///
75/// # Examples
76///
77/// ```no_run
78/// use std::collections::HashMap;
79/// use ironflow_engine::context::PARENT_RUN_ID_LABEL;
80/// use ironflow_store::models::RunFilter;
81/// use uuid::Uuid;
82///
83/// # fn example(parent: Uuid) {
84/// let children = RunFilter {
85///     labels: Some(HashMap::from([(PARENT_RUN_ID_LABEL.to_string(), parent.to_string())])),
86///     ..RunFilter::default()
87/// };
88/// # }
89/// ```
90pub use ironflow_store::entities::PARENT_RUN_ID_LABEL;
91
92pub use steps::agent::AgentReply;
93
94/// Callback type for resolving workflow handlers by name.
95pub(crate) type HandlerResolver =
96    Arc<dyn Fn(&str) -> Option<Arc<dyn WorkflowHandler>> + Send + Sync>;
97
98/// Execution context for a single workflow run.
99///
100/// Tracks the current step position and provides convenience methods
101/// for executing operations with automatic persistence.
102///
103/// # Examples
104///
105/// ```no_run
106/// use ironflow_engine::context::WorkflowContext;
107/// use ironflow_engine::config::ShellConfig;
108/// use ironflow_engine::error::EngineError;
109///
110/// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
111/// let result = ctx.shell("greet", ShellConfig::new("echo hello")).await?;
112/// assert!(result.stdout().contains("hello"));
113/// # Ok(())
114/// # }
115/// ```
116pub struct WorkflowContext {
117    run_id: Uuid,
118    /// The top-level run: `run_id` itself, or the parent's root for a
119    /// sub-workflow. Stamped on agent pods so a retry releases the children.
120    root_run_id: Uuid,
121    workflow_name: String,
122    store: Arc<dyn Store>,
123    provider: Arc<dyn AgentProvider>,
124    /// Optional decision backend (System One / Jev) for `ctx.decision(...)`.
125    /// `None` when no decision provider was wired: a decision step then fails
126    /// explicitly instead of silently doing nothing.
127    decision_provider: Option<Arc<dyn DecisionProvider>>,
128    handler_resolver: Option<HandlerResolver>,
129    position: u32,
130    /// IDs of the last executed step(s) -- used to record DAG dependencies.
131    last_step_ids: Vec<Uuid>,
132    /// Accumulated cost across all steps in this run.
133    total_cost_usd: Decimal,
134    /// Accumulated duration across all steps.
135    total_duration_ms: u64,
136    /// Cumulative cost cap for this run, resolved at creation. `None` = no cap.
137    max_cost_usd: Option<Decimal>,
138    /// Cost already spent by ancestor runs when this context belongs to a
139    /// sub-workflow. Zero for a top-level run.
140    inherited_cost_usd: Decimal,
141    /// Steps from a previous execution of the *same* attempt, keyed by position.
142    /// Used when resuming after approval to replay completed steps.
143    replay_steps: HashMap<u32, Step>,
144    /// All steps of a previous execution of the *same* attempt, keyed by
145    /// `(position, step name)`. A `parallel` wave shares one position across
146    /// several steps, which `replay_steps` cannot represent -- this index lets
147    /// `parallel()` check that every step of a wave already completed before
148    /// replaying the whole wave from the store, without re-running any item.
149    replay_wave_steps: HashMap<(u32, String), Step>,
150    /// Approvals granted in an *earlier* attempt, keyed by position, holding the
151    /// attempt that granted them. An approval is carried by the run, not by the
152    /// attempt, so a retry never asks a human to approve the same gate twice.
153    granted_approvals: HashMap<u32, u32>,
154    /// Human inputs answered in an *earlier* attempt, keyed by position:
155    /// (attempt, answer). Like an approval, an answer is carried by the run, so
156    /// a retry never asks a human to answer the same input twice.
157    answered_inputs: HashMap<u32, (u32, Value)>,
158    /// `Workflow` steps of the current attempt interrupted by a lost worker
159    /// lease, keyed by position. The step executed again at that position
160    /// re-enters the child run they recorded instead of starting a new one.
161    interrupted_children: HashMap<u32, Step>,
162    /// Which run attempt this context is executing (1-based).
163    attempt: u32,
164    /// Wall-clock duration already recorded on the run by previous attempts.
165    /// Added to this attempt's duration when the run is finalized.
166    carried_duration_ms: u64,
167    /// Optional sender for real-time log streaming.
168    log_sender: Option<LogSender>,
169    /// Where artifact bytes are read and written. `None` when no artifact
170    /// storage is configured: steps that declare artifacts then fail explicitly
171    /// instead of silently dropping their files.
172    artifact_sink: Option<Arc<dyn ArtifactSink>>,
173    /// Set to `true` when at least one `allow_failure` step failed.
174    has_allowed_failure: bool,
175    /// Error handlers registered via [`on_error`](Self::on_error).
176    error_handlers: Vec<OnErrorHandler>,
177    /// Shared guard state for workflow execution limits.
178    guard_state: Option<SharedGuardState>,
179    /// Guard configuration for this workflow run.
180    guard_config: Option<WorkflowGuardConfig>,
181    /// Accumulated step results for post-execution inspection.
182    step_results: Vec<StepResult>,
183    /// Optional event bus for per-run real-time monitoring.
184    event_bus: Option<WorkflowEventBus>,
185    /// Optional hook that resolves steps without executing them. `None` in
186    /// production; set by [`crate::testing::TestEngine`].
187    interceptor: Option<Arc<dyn StepInterceptor>>,
188    /// W3C trace context for distributed tracing propagation.
189    trace_context: WorkflowTraceContext,
190    /// Shared operation context for custom operations.
191    operation_ctx: Option<OperationContext>,
192    /// When the run was created, set by the engine. Bounds the signals a wait
193    /// step accepts: a signal received before the run existed is not for it.
194    /// `None` falls back to reading the run from the store.
195    run_created_at: Option<DateTime<Utc>>,
196    /// Set when the context is recording an execution plan instead of running.
197    /// Every step method checks this first and records intent without executing.
198    plan: Option<SharedPlanRecorder>,
199    /// Output set by the handler with [`set_output`](Self::set_output),
200    /// persisted on the run when the execution ends.
201    output: Option<Value>,
202    /// Worker tags carried by the process running this context, set by the
203    /// engine of a tagged worker. A sub-workflow requiring a tag missing from
204    /// this list is refused. `None` outside a worker: no check.
205    worker_tags: Option<Arc<Vec<String>>>,
206}
207
208/// A registered error handler that fires when a subsequent step fails.
209struct OnErrorHandler {
210    name: String,
211    config: StepConfig,
212}
213
214impl fmt::Debug for WorkflowContext {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        f.debug_struct("WorkflowContext")
217            .field("run_id", &self.run_id)
218            .field("position", &self.position)
219            .field("total_cost_usd", &self.total_cost_usd)
220            .field("inherited_cost_usd", &self.inherited_cost_usd)
221            .field("max_cost_usd", &self.max_cost_usd)
222            .field("planning", &self.plan.is_some())
223            .finish_non_exhaustive()
224    }
225}