ironflow_engine/context/steps/
sub_workflow.rs1use std::collections::HashMap;
8use std::time::Instant;
9
10use chrono::Utc;
11use rust_decimal::Decimal;
12use serde_json::{Value, json, to_value};
13use tracing::{error, info};
14
15use ironflow_store::models::{
16 NewRun, NewStep, RunStatus, RunUpdate, StepKind, StepStatus, StepUpdate, TriggerKind,
17 step_trace_id,
18};
19
20use crate::config::WorkflowStepConfig;
21use crate::context::WorkflowContext;
22use crate::error::EngineError;
23use crate::executor::StepOutput;
24use crate::guard::WorkflowRejection;
25use crate::handler::WorkflowHandler;
26
27impl WorkflowContext {
28 pub async fn workflow(
55 &mut self,
56 handler: &dyn WorkflowHandler,
57 payload: Value,
58 ) -> Result<StepOutput, EngineError> {
59 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
61 let state = guard_state
62 .lock()
63 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
64 state.check(guard_config, handler.name())?;
65 }
66
67 let config = WorkflowStepConfig::new(handler.name(), payload);
68 let position = self.position;
69 self.position += 1;
70
71 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
72 let step = self
73 .store
74 .create_step(NewStep {
75 run_id: self.run_id,
76 trace_id,
77 name: config.workflow_name.clone(),
78 kind: StepKind::Workflow,
79 position,
80 input: Some(to_value(&config)?),
81 is_error_handler: false,
82 })
83 .await?;
84
85 self.start_step(step.id, Utc::now()).await?;
86
87 if let Some(guard_state) = &self.guard_state {
89 let mut state = guard_state
90 .lock()
91 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
92 state.record_invocation(handler.name());
93 }
94
95 match self.execute_child_workflow(&config).await {
96 Ok((output, child_had_allowed_failure)) => {
97 self.total_cost_usd += output.cost_usd;
98 self.total_duration_ms += output.duration_ms;
99 if child_had_allowed_failure {
100 self.has_allowed_failure = true;
101 }
102
103 let completed_at = Utc::now();
104 self.store
105 .update_step(
106 step.id,
107 StepUpdate {
108 status: Some(StepStatus::Completed),
109 output: Some(output.output.clone()),
110 duration_ms: Some(output.duration_ms),
111 cost_usd: Some(output.cost_usd),
112 completed_at: Some(completed_at),
113 ..StepUpdate::default()
114 },
115 )
116 .await?;
117
118 info!(
119 run_id = %self.run_id,
120 child_workflow = %config.workflow_name,
121 duration_ms = output.duration_ms,
122 "workflow step completed"
123 );
124
125 self.last_step_ids = vec![step.id];
126
127 self.guard_record_return();
128 Ok(output)
129 }
130 Err(err) => {
131 let completed_at = Utc::now();
132 if let Err(store_err) = self
133 .store
134 .update_step(
135 step.id,
136 StepUpdate {
137 status: Some(StepStatus::Failed),
138 error: Some(err.to_string()),
139 completed_at: Some(completed_at),
140 ..StepUpdate::default()
141 },
142 )
143 .await
144 {
145 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
146 }
147
148 self.guard_record_return();
149 Err(err)
150 }
151 }
152 }
153
154 async fn execute_child_workflow(
157 &self,
158 config: &WorkflowStepConfig,
159 ) -> Result<(StepOutput, bool), EngineError> {
160 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
161 EngineError::InvalidWorkflow(
162 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
163 )
164 })?;
165
166 let handler = resolver(&config.workflow_name).ok_or_else(|| {
167 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
168 })?;
169
170 let parent = self.store.get_run(self.run_id).await?;
173 let (parent_labels, parent_author) =
174 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
175
176 let child_run = self
177 .store
178 .create_run(NewRun {
179 workflow_name: config.workflow_name.clone(),
180 trigger: TriggerKind::Workflow,
181 payload: config.payload.clone(),
182 max_retries: 0,
183 handler_version: None,
184 labels: parent_labels,
185 scheduled_at: None,
186 created_by: parent_author,
187 idempotency_key: None,
188 max_cost_usd: self.max_cost_usd,
190 })
191 .await?
192 .into_run();
193
194 let child_run_id = child_run.id;
195 info!(
196 parent_run_id = %self.run_id,
197 child_run_id = %child_run_id,
198 workflow = %config.workflow_name,
199 "child run created"
200 );
201
202 self.store
203 .update_run_status(child_run_id, RunStatus::Running)
204 .await?;
205
206 let run_start = Instant::now();
207 let mut child_ctx = WorkflowContext {
208 run_id: child_run_id,
209 workflow_name: config.workflow_name.clone(),
210 store: self.store.clone(),
211 provider: self.provider.clone(),
212 decision_provider: self.decision_provider.clone(),
213 handler_resolver: self.handler_resolver.clone(),
214 position: 0,
215 last_step_ids: Vec::new(),
216 total_cost_usd: Decimal::ZERO,
217 total_duration_ms: 0,
218 max_cost_usd: self.max_cost_usd,
219 inherited_cost_usd: self.charged_cost_usd(),
222 replay_steps: HashMap::new(),
223 granted_approvals: HashMap::new(),
224 attempt: 1,
226 carried_duration_ms: 0,
227 log_sender: self.log_sender.clone(),
228 artifact_sink: self.artifact_sink.clone(),
231 has_allowed_failure: false,
232 error_handlers: Vec::new(),
233 guard_state: self.guard_state.clone(),
234 guard_config: self.guard_config.clone(),
235 step_results: Vec::new(),
236 event_bus: self.event_bus.clone(),
237 trace_context: self.trace_context.child(),
238 operation_ctx: None,
239 };
240
241 let result = handler.execute(&mut child_ctx).await;
242 let total_duration = run_start.elapsed().as_millis() as u64;
243 let completed_at = Utc::now();
244
245 match result {
246 Ok(()) => {
247 let child_status = if child_ctx.has_allowed_failure {
248 RunStatus::Warning
249 } else {
250 RunStatus::Completed
251 };
252 self.store
253 .update_run(
254 child_run_id,
255 RunUpdate {
256 status: Some(child_status),
257 cost_usd: Some(child_ctx.total_cost_usd),
258 duration_ms: Some(total_duration),
259 completed_at: Some(completed_at),
260 ..RunUpdate::default()
261 },
262 )
263 .await?;
264
265 let child_had_allowed_failure = child_ctx.has_allowed_failure;
266 Ok((
267 StepOutput {
268 output: json!({
269 "run_id": child_run_id,
270 "workflow_name": config.workflow_name,
271 "status": child_status,
272 "cost_usd": child_ctx.total_cost_usd,
273 "duration_ms": total_duration,
274 }),
275 duration_ms: total_duration,
276 cost_usd: child_ctx.total_cost_usd,
277 input_tokens: None,
278 output_tokens: None,
279 model: None,
280 debug_messages: None,
281 },
282 child_had_allowed_failure,
283 ))
284 }
285 Err(err) => {
286 if let Err(store_err) = self
287 .store
288 .update_run(
289 child_run_id,
290 RunUpdate {
291 status: Some(RunStatus::Failed),
292 error: Some(err.to_string()),
293 cost_usd: Some(child_ctx.total_cost_usd),
294 duration_ms: Some(total_duration),
295 completed_at: Some(completed_at),
296 ..RunUpdate::default()
297 },
298 )
299 .await
300 {
301 error!(
302 child_run_id = %child_run_id,
303 store_error = %store_err,
304 "failed to persist child run failure"
305 );
306 }
307
308 Err(err)
309 }
310 }
311 }
312}