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};
14use uuid::Uuid;
15
16use ironflow_store::models::{
17 NewRun, NewStep, RunStatus, RunUpdate, StepKind, StepStatus, StepUpdate, TriggerKind,
18 step_trace_id,
19};
20
21use crate::config::{StepConfig, WorkflowStepConfig};
22use crate::context::WorkflowContext;
23use crate::error::EngineError;
24use crate::executor::StepOutput;
25use crate::guard::WorkflowRejection;
26use crate::handler::WorkflowHandler;
27use crate::plan::{SharedPlanRecorder, lock_plan, planned_output};
28
29impl WorkflowContext {
30 pub async fn workflow(
57 &mut self,
58 handler: &dyn WorkflowHandler,
59 payload: Value,
60 ) -> Result<StepOutput, EngineError> {
61 if let Some(plan) = self.plan().cloned() {
65 return self.plan_sub_workflow(&plan, handler, payload).await;
66 }
67
68 if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
70 let state = guard_state
71 .lock()
72 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
73 state.check(guard_config, handler.name())?;
74 }
75
76 let config = WorkflowStepConfig::new(handler.name(), payload);
77 let position = self.position;
78 self.position += 1;
79
80 let trace_id = step_trace_id(self.run_id, &config.workflow_name, position);
81 let step = self
82 .store
83 .create_step(NewStep {
84 run_id: self.run_id,
85 trace_id,
86 name: config.workflow_name.clone(),
87 kind: StepKind::Workflow,
88 position,
89 input: Some(to_value(&config)?),
90 is_error_handler: false,
91 })
92 .await?;
93
94 self.start_step(step.id, Utc::now()).await?;
95
96 if let Some(guard_state) = &self.guard_state {
98 let mut state = guard_state
99 .lock()
100 .map_err(|_| WorkflowRejection::GuardUnavailable)?;
101 state.record_invocation(handler.name());
102 }
103
104 match self.execute_child_workflow(&config).await {
105 Ok((output, child_had_allowed_failure)) => {
106 self.total_cost_usd += output.cost_usd;
107 self.total_duration_ms += output.duration_ms;
108 if child_had_allowed_failure {
109 self.has_allowed_failure = true;
110 }
111
112 let completed_at = Utc::now();
113 self.store
114 .update_step(
115 step.id,
116 StepUpdate {
117 status: Some(StepStatus::Completed),
118 output: Some(output.output.clone()),
119 duration_ms: Some(output.duration_ms),
120 cost_usd: Some(output.cost_usd),
121 completed_at: Some(completed_at),
122 ..StepUpdate::default()
123 },
124 )
125 .await?;
126
127 info!(
128 run_id = %self.run_id,
129 child_workflow = %config.workflow_name,
130 duration_ms = output.duration_ms,
131 "workflow step completed"
132 );
133
134 self.last_step_ids = vec![step.id];
135
136 self.guard_record_return();
137 Ok(output)
138 }
139 Err(err) => {
140 let completed_at = Utc::now();
141 if let Err(store_err) = self
142 .store
143 .update_step(
144 step.id,
145 StepUpdate {
146 status: Some(StepStatus::Failed),
147 error: Some(err.to_string()),
148 completed_at: Some(completed_at),
149 ..StepUpdate::default()
150 },
151 )
152 .await
153 {
154 error!(step_id = %step.id, error = %store_err, "failed to persist step failure");
155 }
156
157 self.guard_record_return();
158 Err(err)
159 }
160 }
161 }
162
163 async fn plan_sub_workflow(
169 &mut self,
170 plan: &SharedPlanRecorder,
171 handler: &dyn WorkflowHandler,
172 payload: Value,
173 ) -> Result<StepOutput, EngineError> {
174 self.position += 1;
175 let sub_name = handler.name().to_string();
176
177 {
178 let mut recorder = lock_plan(plan);
179 if !recorder.record(&sub_name, StepKind::Workflow, &self.workflow_name, None) {
180 return Ok(planned_output(
181 &StepConfig::Workflow(WorkflowStepConfig::new(&sub_name, payload)),
182 None,
183 ));
184 }
185 recorder.set_last(vec![sub_name.clone()]);
186 }
187
188 let expand = lock_plan(plan).enter_workflow();
189 if expand {
190 let previous_payload = lock_plan(plan).swap_payload(payload.clone());
191
192 let mut child = WorkflowContext::new(
193 Uuid::now_v7(),
194 sub_name.clone(),
195 self.store.clone(),
196 self.provider.clone(),
197 );
198 child.handler_resolver = self.handler_resolver.clone();
199 child.set_plan(plan.clone());
200
201 if let Err(err) = handler.execute(&mut child).await {
202 lock_plan(plan).fail(format!(
203 "sub-workflow {sub_name} could not be planned: {err}"
204 ));
205 }
206
207 let mut recorder = lock_plan(plan);
208 recorder.swap_payload(previous_payload);
209 recorder.leave_workflow();
210 }
211
212 Ok(planned_output(
213 &StepConfig::Workflow(WorkflowStepConfig::new(&sub_name, payload)),
214 None,
215 ))
216 }
217
218 async fn execute_child_workflow(
221 &self,
222 config: &WorkflowStepConfig,
223 ) -> Result<(StepOutput, bool), EngineError> {
224 let resolver = self.handler_resolver.as_ref().ok_or_else(|| {
225 EngineError::InvalidWorkflow(
226 "sub-workflow requires a handler resolver (use Engine to execute)".to_string(),
227 )
228 })?;
229
230 let handler = resolver(&config.workflow_name).ok_or_else(|| {
231 EngineError::InvalidWorkflow(format!("no handler registered: {}", config.workflow_name))
232 })?;
233
234 let parent = self.store.get_run(self.run_id).await?;
237 let (parent_labels, parent_author) =
238 parent.map(|r| (r.labels, r.created_by)).unwrap_or_default();
239
240 let child_run = self
241 .store
242 .create_run(NewRun {
243 workflow_name: config.workflow_name.clone(),
244 trigger: TriggerKind::Workflow,
245 payload: config.payload.clone(),
246 max_retries: 0,
247 handler_version: None,
248 labels: parent_labels,
249 scheduled_at: None,
250 created_by: parent_author,
251 idempotency_key: None,
252 max_cost_usd: self.max_cost_usd,
254 })
255 .await?
256 .into_run();
257
258 let child_run_id = child_run.id;
259 info!(
260 parent_run_id = %self.run_id,
261 child_run_id = %child_run_id,
262 workflow = %config.workflow_name,
263 "child run created"
264 );
265
266 self.store
267 .update_run_status(child_run_id, RunStatus::Running)
268 .await?;
269
270 let run_start = Instant::now();
271 let mut child_ctx = WorkflowContext {
272 run_id: child_run_id,
273 workflow_name: config.workflow_name.clone(),
274 store: self.store.clone(),
275 provider: self.provider.clone(),
276 decision_provider: self.decision_provider.clone(),
277 handler_resolver: self.handler_resolver.clone(),
278 position: 0,
279 last_step_ids: Vec::new(),
280 total_cost_usd: Decimal::ZERO,
281 total_duration_ms: 0,
282 max_cost_usd: self.max_cost_usd,
283 inherited_cost_usd: self.charged_cost_usd(),
286 replay_steps: HashMap::new(),
287 granted_approvals: HashMap::new(),
288 attempt: 1,
290 carried_duration_ms: 0,
291 log_sender: self.log_sender.clone(),
292 artifact_sink: self.artifact_sink.clone(),
295 has_allowed_failure: false,
296 error_handlers: Vec::new(),
297 guard_state: self.guard_state.clone(),
298 guard_config: self.guard_config.clone(),
299 step_results: Vec::new(),
300 event_bus: self.event_bus.clone(),
301 trace_context: self.trace_context.child(),
302 operation_ctx: None,
303 plan: None,
304 };
305
306 let result = handler.execute(&mut child_ctx).await;
307 let total_duration = run_start.elapsed().as_millis() as u64;
308 let completed_at = Utc::now();
309
310 match result {
311 Ok(()) => {
312 let child_status = if child_ctx.has_allowed_failure {
313 RunStatus::Warning
314 } else {
315 RunStatus::Completed
316 };
317 self.store
318 .update_run(
319 child_run_id,
320 RunUpdate {
321 status: Some(child_status),
322 cost_usd: Some(child_ctx.total_cost_usd),
323 duration_ms: Some(total_duration),
324 completed_at: Some(completed_at),
325 ..RunUpdate::default()
326 },
327 )
328 .await?;
329
330 let child_had_allowed_failure = child_ctx.has_allowed_failure;
331 Ok((
332 StepOutput {
333 output: json!({
334 "run_id": child_run_id,
335 "workflow_name": config.workflow_name,
336 "status": child_status,
337 "cost_usd": child_ctx.total_cost_usd,
338 "duration_ms": total_duration,
339 }),
340 duration_ms: total_duration,
341 cost_usd: child_ctx.total_cost_usd,
342 input_tokens: None,
343 output_tokens: None,
344 model: None,
345 debug_messages: None,
346 },
347 child_had_allowed_failure,
348 ))
349 }
350 Err(err) => {
351 if let Err(store_err) = self
352 .store
353 .update_run(
354 child_run_id,
355 RunUpdate {
356 status: Some(RunStatus::Failed),
357 error: Some(err.to_string()),
358 cost_usd: Some(child_ctx.total_cost_usd),
359 duration_ms: Some(total_duration),
360 completed_at: Some(completed_at),
361 ..RunUpdate::default()
362 },
363 )
364 .await
365 {
366 error!(
367 child_run_id = %child_run_id,
368 store_error = %store_err,
369 "failed to persist child run failure"
370 );
371 }
372
373 Err(err)
374 }
375 }
376 }
377}