1use super::events::kinds;
13use super::reactor::Runtime;
14use crate::engine::model::Step;
15use crate::engine::run::RunStatus;
16use crate::state::now_ms;
17use serde_json::{Map, Value, json};
18
19type StartSpec = (String, String, String, Map<String, Value>);
21
22impl Runtime {
23 fn start_key(workflow: &str, node: &str) -> String {
25 format!("{workflow}.{node}")
26 }
27
28 fn start_state(&self, workflow: &str, node: &str) -> Value {
30 self.durable
31 .manifest()
32 .starts
33 .get(&Self::start_key(workflow, node))
34 .cloned()
35 .unwrap_or(json!({}))
36 }
37
38 fn set_start_state(&mut self, workflow: &str, node: &str, state: Value) {
40 let key = Self::start_key(workflow, node);
41 self.durable.manifest_update(|m| {
42 m.starts.insert(key, state);
43 });
44 }
45
46 pub(crate) fn arm_long_lived_starts(&mut self) {
50 self.repair_orphaned_timer_waits();
55 let specs: Vec<StartSpec> = self
56 .workflows
57 .values()
58 .filter(|w| w.armed)
59 .flat_map(|w| {
60 w.start_steps()
61 .into_iter()
62 .map(|s| (w.name.clone(), s.id.clone(), s.kind.clone(), s.spec.clone()))
63 .collect::<Vec<_>>()
64 })
65 .collect();
66 for (workflow, node, kind, spec) in specs {
67 match kind.as_str() {
68 "schedule" => self.arm_schedule(&workflow, &node, &spec),
69 "loop" => {
70 let live = self
72 .runs
73 .values()
74 .any(|r| r.workflow == workflow && !r.status.is_terminal());
75 let iteration = self.start_state(&workflow, &node)["iteration"]
76 .as_u64()
77 .unwrap_or(0);
78 let max = spec.get("max_iterations").and_then(Value::as_u64);
79 if !live && max.is_none_or(|m| iteration < m) {
80 let delay = spec
81 .get("delay")
82 .and_then(Value::as_str)
83 .and_then(|d| crate::config::parse_duration(d).ok());
84 match delay {
85 Some(d) if !d.is_zero() => self.set_start_state(&workflow, &node, json!({"iteration": iteration, "next_ms": now_ms() + d.as_millis() as u64})),
86 _ => self.fire_start(&workflow, &node, &spec, json!({"iteration": iteration}), "loop"),
87 }
88 }
89 }
90 "subscribe" => self.arm_subscribe(&workflow, &node, &spec),
91 _ => {}
92 }
93 }
94 }
95
96 fn arm_schedule(&mut self, workflow: &str, node: &str, spec: &Map<String, Value>) {
97 let st = self.start_state(workflow, node);
98 let at_fired = st["at_fired"].as_bool().unwrap_or(false);
99 let next = self.next_schedule_ms(spec, now_ms(), at_fired);
100 if let Some(next) = next {
101 let mut st = st;
102 st["next_ms"] = json!(next);
103 self.set_start_state(workflow, node, st);
104 self.log.info(
105 "start.schedule.armed",
106 json!({"workflow": workflow, "node": node, "next_ms": next}),
107 );
108 } else if at_fired {
109 self.log.info(
112 "start.schedule.done",
113 json!({"workflow": workflow, "node": node, "note": "one-shot `at` already fired"}),
114 );
115 } else {
116 self.log.warn(
117 "start.schedule.invalid",
118 json!({"workflow": workflow, "node": node, "note": "no cron/every"}),
119 );
120 }
121 }
122
123 fn next_schedule_ms(
127 &self,
128 spec: &Map<String, Value>,
129 after_ms: u64,
130 at_fired: bool,
131 ) -> Option<u64> {
132 if let Some(every) = spec
133 .get("every")
134 .and_then(Value::as_str)
135 .and_then(|e| crate::config::parse_duration(e).ok())
136 {
137 return Some(after_ms + every.as_millis() as u64);
138 }
139 if let Some(at) = spec
140 .get("at")
141 .and_then(Value::as_str)
142 .filter(|_| !at_fired)
143 .and_then(|a| crate::config::parse_duration(a).ok())
144 {
145 return Some(now_ms() + at.as_millis() as u64);
148 }
149 #[cfg(feature = "cron")]
150 if let Some(cron) = spec.get("cron").and_then(Value::as_str) {
151 return crate::triggers::timer::CronExpr::parse(cron)
152 .ok()
153 .and_then(|c| c.next_after(after_ms / 1000))
154 .map(|s| s * 1000);
155 }
156 None
157 }
158
159 fn arm_subscribe(&mut self, workflow: &str, node: &str, spec: &Map<String, Value>) {
160 let server = spec.get("server").and_then(Value::as_str).unwrap_or("");
161 let uri = spec.get("uri").and_then(Value::as_str).unwrap_or("");
162 match self.mcp.get(server) {
163 Some(c) => match c.subscribe(uri) {
164 Ok(()) => self.log.info(
165 "start.subscribe.armed",
166 json!({"workflow": workflow, "node": node, "server": server, "uri": uri}),
167 ),
168 Err(e) => self.log.warn(
169 "start.subscribe.fail",
170 json!({"workflow": workflow, "node": node, "err": e.to_string()}),
171 ),
172 },
173 None => self.log.warn(
174 "start.subscribe.no_server",
175 json!({"workflow": workflow, "node": node, "server": server}),
176 ),
177 }
178 }
179
180 pub(crate) fn poll_starts(&mut self) {
183 let now = now_ms();
184 let due: Vec<StartSpec> = self
185 .workflows
186 .values()
187 .filter(|w| w.armed)
188 .flat_map(|w| {
189 w.start_steps()
190 .into_iter()
191 .filter(|s| matches!(s.kind.as_str(), "schedule" | "loop" | "subscribe"))
192 .map(|s| (w.name.clone(), s.id.clone(), s.kind.clone(), s.spec.clone()))
193 .collect::<Vec<_>>()
194 })
195 .collect();
196 for (workflow, node, kind, spec) in due {
197 let st = self.start_state(&workflow, &node);
198 match kind.as_str() {
199 "schedule" => {
200 if let Some(next) = st["next_ms"].as_u64()
201 && now >= next
202 {
203 let at_fired =
212 st["at_fired"].as_bool().unwrap_or(false) || spec.contains_key("at");
213 self.fire_start(
214 &workflow,
215 &node,
216 &spec,
217 json!({"scheduled_for": next}),
218 "schedule",
219 );
220 let following = self.next_schedule_ms(&spec, now, at_fired);
222 let mut st = self.start_state(&workflow, &node);
223 match following {
224 Some(n) => st["next_ms"] = json!(n),
225 None => {
226 st.as_object_mut().map(|o| o.remove("next_ms"));
227 }
228 }
229 if at_fired {
230 st["at_fired"] = json!(true);
231 }
232 st["last_fired"] = json!(now);
233 self.set_start_state(&workflow, &node, st);
234 }
235 }
236 "loop" => {
237 let live = self
238 .runs
239 .values()
240 .any(|r| r.workflow == workflow && !r.status.is_terminal());
241 if !live
242 && let Some(next) = st["next_ms"].as_u64()
243 && now >= next
244 {
245 let mut st2 = self.start_state(&workflow, &node);
248 st2.as_object_mut().map(|o| o.remove("next_ms"));
249 self.set_start_state(&workflow, &node, st2);
250 let iteration = st["iteration"].as_u64().unwrap_or(0);
251 let max = spec.get("max_iterations").and_then(Value::as_u64);
252 if max.is_none_or(|m| iteration < m) {
253 self.fire_start(
254 &workflow,
255 &node,
256 &spec,
257 json!({"iteration": iteration}),
258 "loop",
259 );
260 }
261 }
262 }
263 "subscribe" => {
264 if let Some(fire_at) = st["debounce_until"].as_u64()
266 && now >= fire_at
267 {
268 let payload = st["pending_payload"].clone();
269 let mut st2 = self.start_state(&workflow, &node);
270 st2.as_object_mut().map(|o| {
271 o.remove("debounce_until");
272 o.remove("pending_payload")
273 });
274 self.set_start_state(&workflow, &node, st2);
275 self.fire_start(&workflow, &node, &spec, payload, "subscribe");
276 }
277 }
278 _ => {}
279 }
280 }
281 }
282
283 pub(crate) fn fire_start(
287 &mut self,
288 workflow: &str,
289 node: &str,
290 spec: &Map<String, Value>,
291 payload: Value,
292 kind: &str,
293 ) {
294 self.fire_start_run(workflow, node, spec, payload, kind, None);
295 }
296
297 pub(crate) fn fire_start_run(
300 &mut self,
301 workflow: &str,
302 node: &str,
303 spec: &Map<String, Value>,
304 payload: Value,
305 kind: &str,
306 run_id: Option<&str>,
307 ) {
308 let inputs = match spec.get("inputs") {
309 Some(mapping) => {
310 let mut data = crate::engine::template::Data::new();
311 data.insert("payload".into(), payload.clone());
312 data.insert(
313 "env".into(),
314 json!({"instance": self.instance, "ts": now_ms()}),
315 );
316 crate::engine::template::render(mapping, &data).unwrap_or(json!({}))
317 }
318 None => json!({}),
319 };
320 self.log.info(
321 "start.fired",
322 json!({"workflow": workflow, "node": node, "kind": kind}),
323 );
324 let mut st = self.start_state(workflow, node);
325 st["last_fired"] = json!(now_ms());
326 if kind == "loop" {
327 st["iteration"] = json!(st["iteration"].as_u64().unwrap_or(0) + 1);
328 }
329 self.set_start_state(workflow, node, st);
330 let mut ev =
331 json!({"workflow": workflow, "node": node, "payload": payload, "inputs": inputs});
332 if let Some(rid) = run_id {
333 ev["run_id"] = json!(rid);
334 }
335 let _ = self.accept_event(kinds::START_FIRED, None, ev);
336 }
337
338 pub(crate) fn on_loop_run_finished(
341 &mut self,
342 workflow: &str,
343 node: &str,
344 spec: &Map<String, Value>,
345 ok: bool,
346 last_output: &Value,
347 ) {
348 if let Some(until) = spec.get("until").and_then(Value::as_str) {
350 let mut data = crate::engine::template::Data::new();
351 data.insert("outcome".into(), json!({"ok": ok, "output": last_output}));
352 data.insert("last".into(), last_output.clone());
353 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
354 if crate::cel::eval_bool(until.trim().trim_start_matches("CEL:").trim(), &vars)
355 == Ok(true)
356 {
357 self.log.info(
358 "start.loop.stopped",
359 json!({"workflow": workflow, "node": node, "reason": "until"}),
360 );
361 let mut st = self.start_state(workflow, node);
362 st.as_object_mut().map(|o| o.remove("next_ms"));
363 self.set_start_state(workflow, node, st);
364 return;
365 }
366 }
367 let st = self.start_state(workflow, node);
368 let iteration = st["iteration"].as_u64().unwrap_or(0);
369 if let Some(max) = spec.get("max_iterations").and_then(Value::as_u64)
370 && iteration >= max
371 {
372 self.log.info(
373 "start.loop.stopped",
374 json!({"workflow": workflow, "node": node, "reason": "max_iterations"}),
375 );
376 let mut st = self.start_state(workflow, node);
377 st.as_object_mut().map(|o| o.remove("next_ms"));
378 self.set_start_state(workflow, node, st);
379 return;
380 }
381 let interval = spec
383 .get("interval")
384 .and_then(Value::as_str)
385 .and_then(|i| crate::config::parse_duration(i).ok())
386 .map(|d| d.as_millis() as u64)
387 .unwrap_or(0);
388 let delay = if !ok {
389 spec.get("backoff")
390 .and_then(|b| b.get("initial"))
391 .and_then(Value::as_str)
392 .and_then(|i| crate::config::parse_duration(i).ok())
393 .map(|d| d.as_millis() as u64)
394 .unwrap_or(interval)
395 } else {
396 interval
397 };
398 let mut st = self.start_state(workflow, node);
399 st["next_ms"] = json!(now_ms() + delay);
400 self.set_start_state(workflow, node, st);
401 }
402
403 pub(crate) fn on_subscribe_resource(&mut self, server: &str, uri: &str) {
405 let matches: Vec<(String, String, Map<String, Value>)> = self
406 .workflows
407 .values()
408 .filter(|w| w.armed)
409 .flat_map(|w| {
410 w.start_steps()
411 .into_iter()
412 .filter(|s| {
413 s.kind == "subscribe"
414 && s.field_str("server") == Some(server)
415 && s.field_str("uri") == Some(uri)
416 })
417 .map(|s| (w.name.clone(), s.id.clone(), s.spec.clone()))
418 .collect::<Vec<_>>()
419 })
420 .collect();
421 if matches.is_empty() {
422 return;
423 }
424 let content = self
426 .mcp
427 .get(server)
428 .and_then(|c| c.read_resource(uri).ok())
429 .map(|r| {
430 let t = r.text();
431 serde_json::from_str::<Value>(&t).unwrap_or(Value::String(t))
432 });
433 for (workflow, node, spec) in matches {
434 if let Some(filter) = spec.get("filter").and_then(Value::as_str) {
436 let mut data = crate::engine::template::Data::new();
437 data.insert("content".into(), content.clone().unwrap_or(Value::Null));
438 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
439 if crate::cel::eval_bool(filter.trim().trim_start_matches("CEL:").trim(), &vars)
440 != Ok(true)
441 {
442 continue;
443 }
444 }
445 let payload = json!({"server": server, "uri": uri, "content": content});
446 let debounce = spec.get("debounce_ms").and_then(Value::as_u64).unwrap_or(0);
447 if debounce > 0 {
448 let mut st = self.start_state(&workflow, &node);
450 st["debounce_until"] = json!(now_ms() + debounce);
451 st["pending_payload"] = payload;
452 self.set_start_state(&workflow, &node, st);
453 } else {
454 self.fire_start(&workflow, &node, &spec, payload, "subscribe");
455 }
456 }
457 }
458
459 pub(crate) fn fire_signal_starts(
461 &mut self,
462 name: &str,
463 payload: &Value,
464 _broadcast: bool,
465 ) -> u64 {
466 let matches: Vec<(String, String, Map<String, Value>)> = self
467 .workflows
468 .values()
469 .filter(|w| w.armed)
470 .flat_map(|w| {
471 w.start_steps()
472 .into_iter()
473 .filter(|s| s.kind == "signal" && s.field_str("name") == Some(name))
474 .map(|s| (w.name.clone(), s.id.clone(), s.spec.clone()))
475 .collect::<Vec<_>>()
476 })
477 .collect();
478 let mut fired = 0;
479 for (workflow, node, spec) in matches {
480 if let Some(filter) = spec.get("filter").and_then(Value::as_str) {
481 let mut data = crate::engine::template::Data::new();
482 data.insert("payload".into(), payload.clone());
483 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
484 if crate::cel::eval_bool(filter.trim().trim_start_matches("CEL:").trim(), &vars)
485 != Ok(true)
486 {
487 continue;
488 }
489 }
490 self.fire_start(
491 &workflow,
492 &node,
493 &spec,
494 json!({"signal": name, "payload": payload}),
495 "signal",
496 );
497 fired += 1;
498 }
499 fired
500 }
501
502 pub(crate) fn fire_event_starts(&mut self, event: &str, payload: &Value) {
504 let matches: Vec<(String, String, Map<String, Value>)> = self
505 .workflows
506 .values()
507 .filter(|w| w.armed)
508 .flat_map(|w| {
509 w.start_steps()
510 .into_iter()
511 .filter(|s| s.kind == "event" && s.field_str("on") == Some(event))
512 .map(|s| (w.name.clone(), s.id.clone(), s.spec.clone()))
513 .collect::<Vec<_>>()
514 })
515 .collect();
516 for (workflow, node, spec) in matches {
517 if let Some(filter) = spec.get("filter").and_then(Value::as_str) {
518 let mut data = crate::engine::template::Data::new();
519 data.insert("payload".into(), payload.clone());
520 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
521 if crate::cel::eval_bool(filter.trim().trim_start_matches("CEL:").trim(), &vars)
522 != Ok(true)
523 {
524 continue;
525 }
526 }
527 self.fire_start(
528 &workflow,
529 &node,
530 &spec,
531 json!({"event": event, "payload": payload}),
532 "event",
533 );
534 }
535 }
536
537 pub(crate) fn run_start_spec(
539 &self,
540 run_id: &str,
541 ) -> Option<(String, String, Map<String, Value>, String)> {
542 let run = self.runs.get(run_id)?;
543 let w = self.workflows.get(&run.workflow)?;
544 let s: &Step = w.step(&run.start.node)?;
545 Some((
546 run.workflow.clone(),
547 run.start.node.clone(),
548 s.spec.clone(),
549 s.kind.clone(),
550 ))
551 }
552}
553
554pub fn run_event(status: RunStatus) -> Option<&'static str> {
556 match status {
557 RunStatus::Completed => Some("workflow.finished"),
558 RunStatus::Failed | RunStatus::Stalled | RunStatus::Cancelled | RunStatus::Refused => {
559 Some("workflow.failed")
560 }
561 _ => None,
562 }
563}