Skip to main content

ironflow_engine/context/steps/
operation.rs

1//! Custom operation step for [`WorkflowContext`].
2
3use std::time::Instant;
4
5use chrono::Utc;
6use rust_decimal::Decimal;
7use tracing::{error, info};
8
9use ironflow_store::models::{NewStep, StepKind, StepStatus, StepUpdate, step_trace_id};
10
11use crate::context::WorkflowContext;
12use crate::error::EngineError;
13use crate::executor::{StepArtifacts, StepOutput};
14use crate::operation::Operation;
15use crate::plan::{lock_plan, planned_custom_output};
16
17impl WorkflowContext {
18    /// Execute a custom operation step.
19    ///
20    /// Runs a user-defined [`Operation`] with full step lifecycle management:
21    /// creates the step record, transitions to Running, executes the operation,
22    /// persists the output and duration, and marks the step Completed or Failed.
23    ///
24    /// The operation's [`kind()`](Operation::kind) is stored as
25    /// [`StepKind::Custom`].
26    ///
27    /// On resume, a step that already completed in a prior execution of the
28    /// current attempt is replayed from the store instead of calling
29    /// [`Operation::execute`] again.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`EngineError`] if the operation fails or the store errors.
34    /// Returns [`EngineError::ReplayDivergence`] when the step recorded at
35    /// this position has a different name or kind.
36    ///
37    /// # Examples
38    ///
39    /// ```no_run
40    /// use async_trait::async_trait;
41    /// use ironflow_engine::context::WorkflowContext;
42    /// use ironflow_engine::operation::{Operation, OperationContext};
43    /// use ironflow_core::error::OperationError;
44    /// use ironflow_engine::error::EngineError;
45    /// use serde_json::{Value, json};
46    ///
47    /// struct MyOp;
48    /// #[async_trait]
49    /// impl Operation for MyOp {
50    ///     fn kind(&self) -> &str { "my-service" }
51    ///     async fn execute(&self, _ctx: &OperationContext) -> Result<Value, OperationError> {
52    ///         Ok(json!({"ok": true}))
53    ///     }
54    /// }
55    ///
56    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
57    /// let result = ctx.operation("call-service", &MyOp).await?;
58    /// println!("output: {}", result.output);
59    /// # Ok(())
60    /// # }
61    /// ```
62    pub async fn operation(
63        &mut self,
64        name: &str,
65        op: &dyn Operation,
66    ) -> Result<StepOutput, EngineError> {
67        let kind = StepKind::Custom(op.kind().to_string());
68
69        // Plan mode: `op.execute` is never called, so no third-party API is
70        // touched while planning.
71        if let Some(plan) = self.plan().cloned() {
72            self.position += 1;
73            let mut recorder = lock_plan(&plan);
74            let estimate = recorder.estimate_for(name);
75            recorder.record_or_stop(name, kind, &self.workflow_name, None)?;
76            recorder.set_last(vec![name.to_string()]);
77            let mut output = planned_custom_output(estimate);
78            output.artifacts = StepArtifacts::new(name, None, &[]);
79            return Ok(output);
80        }
81
82        let position = self.position;
83        self.position += 1;
84
85        // Replay: if this step already completed in a prior execution of the
86        // current attempt, return its cached output without calling
87        // `op.execute` or creating a new step.
88        if let Some(mut output) = self.try_replay_step(position, name, &kind)? {
89            let step_id = self.last_step_ids.last().copied();
90            output.artifacts = StepArtifacts::new(name, step_id, &[]);
91            return Ok(output);
92        }
93
94        let trace_id = step_trace_id(self.run_id, name, position);
95        let step = self
96            .store
97            .create_step(NewStep {
98                run_id: self.run_id,
99                trace_id,
100                name: name.to_string(),
101                kind,
102                position,
103                input: op.input(),
104                is_error_handler: false,
105            })
106            .await?;
107
108        self.start_step(step.id, Utc::now()).await?;
109
110        let start = Instant::now();
111
112        let op_ctx = self.ensure_operation_ctx();
113
114        match op.execute(op_ctx).await {
115            Ok(output_value) => {
116                let duration_ms = start.elapsed().as_millis() as u64;
117                self.total_duration_ms += duration_ms;
118
119                let completed_at = Utc::now();
120                let step_id = self
121                    .complete_step(
122                        &step,
123                        StepUpdate {
124                            status: Some(StepStatus::Completed),
125                            output: Some(output_value.clone()),
126                            duration_ms: Some(duration_ms),
127                            cost_usd: Some(Decimal::ZERO),
128                            completed_at: Some(completed_at),
129                            ..StepUpdate::default()
130                        },
131                    )
132                    .await?;
133
134                info!(
135                    run_id = %self.run_id,
136                    step = %name,
137                    kind = op.kind(),
138                    duration_ms,
139                    "operation step completed"
140                );
141
142                self.last_step_ids = vec![step_id];
143
144                Ok(StepOutput {
145                    output: output_value,
146                    duration_ms,
147                    cost_usd: Decimal::ZERO,
148                    input_tokens: None,
149                    cache_read_input_tokens: None,
150                    cache_creation_input_tokens: None,
151                    output_tokens: None,
152                    model: None,
153                    debug_messages: None,
154                    // An operation declares no output; its record is what
155                    // `put_artifact` attaches bytes to.
156                    artifacts: StepArtifacts::new(name, Some(step_id), &[]),
157                    account_id: None,
158                    environment_id: None,
159                })
160            }
161            Err(err) => {
162                let completed_at = Utc::now();
163                let engine_err = EngineError::Operation(err);
164                if let Err(store_err) = self
165                    .store
166                    .update_step(
167                        step.id,
168                        StepUpdate {
169                            status: Some(StepStatus::Failed),
170                            error: Some(engine_err.to_string()),
171                            completed_at: Some(completed_at),
172                            ..StepUpdate::default()
173                        },
174                    )
175                    .await
176                {
177                    error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
178                }
179
180                Err(engine_err)
181            }
182        }
183    }
184}