Skip to main content

ironflow_engine/config/
mod.rs

1//! Serializable step configurations — one per operation type.
2//!
3//! These types mirror the builder options in [`ironflow_core`] operations but
4//! are fully serializable, allowing them to be stored as JSON in the database
5//! and reconstructed by the executor at runtime.
6
7mod agent;
8mod approval;
9mod approval_rule;
10mod artifact;
11mod decision;
12pub mod delay;
13mod escalation;
14mod http;
15mod shell;
16mod workflow;
17
18pub use agent::AgentStepConfig;
19pub use approval::ApprovalConfig;
20pub use approval_rule::ApprovalRule;
21pub use artifact::{ArtifactInput, ArtifactOutput};
22pub use decision::{DEFAULT_DECISION_MODEL, DecisionConfig};
23pub use delay::DelayConfig;
24pub use escalation::{EscalationPolicy, NotificationTarget};
25pub use http::HttpConfig;
26// Re-exported so workflow authors can name approval assignees and read approval
27// requirements without depending on `ironflow-store` directly.
28pub use ironflow_store::entities::{ApprovalRequirement, Assignee};
29pub use shell::ShellConfig;
30pub use workflow::WorkflowStepConfig;
31
32pub use crate::expression::{Expression, ExpressionError};
33
34use ironflow_core::retry::RetryPolicy;
35use ironflow_store::entities::StepKind;
36use serde::{Deserialize, Serialize};
37
38/// A serializable step configuration, wrapping one of the operation-specific configs.
39///
40/// Stored as JSON in the `steps.input` column and reconstructed by the
41/// executor at runtime.
42///
43/// # Examples
44///
45/// ```
46/// use ironflow_engine::config::{StepConfig, ShellConfig};
47///
48/// let config = StepConfig::Shell(ShellConfig::new("echo hello"));
49/// let json = serde_json::to_string(&config).unwrap();
50/// assert!(json.contains("echo hello"));
51/// ```
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(tag = "type", rename_all = "snake_case")]
54pub enum StepConfig {
55    /// A shell command step.
56    Shell(ShellConfig),
57    /// An HTTP request step.
58    Http(HttpConfig),
59    /// An AI agent step.
60    Agent(AgentStepConfig),
61    /// A sub-workflow invocation step.
62    Workflow(WorkflowStepConfig),
63    /// A human approval gate step.
64    Approval(ApprovalConfig),
65    /// A typed machine-decision step (System One / Jev).
66    Decision(DecisionConfig),
67    /// A timed delay/sleep step.
68    Delay(DelayConfig),
69}
70
71impl StepConfig {
72    /// Whether this step is allowed to fail without stopping the run.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use ironflow_engine::config::{StepConfig, ShellConfig};
78    ///
79    /// let config = StepConfig::Shell(ShellConfig::new("cargo clippy").allow_failure());
80    /// assert!(config.allow_failure());
81    ///
82    /// let config = StepConfig::Shell(ShellConfig::new("cargo build"));
83    /// assert!(!config.allow_failure());
84    /// ```
85    pub fn allow_failure(&self) -> bool {
86        match self {
87            StepConfig::Shell(c) => c.allow_failure,
88            StepConfig::Http(c) => c.allow_failure,
89            StepConfig::Agent(c) => c.allow_failure,
90            StepConfig::Workflow(_)
91            | StepConfig::Approval(_)
92            | StepConfig::Decision(_)
93            | StepConfig::Delay(_) => false,
94        }
95    }
96
97    /// Get the step-level retry policy, if any.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use ironflow_core::retry::RetryPolicy;
103    /// use ironflow_engine::config::{StepConfig, ShellConfig};
104    ///
105    /// let config = StepConfig::Shell(ShellConfig::new("echo test").retry_policy(RetryPolicy::new(3)));
106    /// assert!(config.retry().is_some());
107    ///
108    /// let config = StepConfig::Shell(ShellConfig::new("echo test"));
109    /// assert!(config.retry().is_none());
110    /// ```
111    pub fn retry(&self) -> Option<&RetryPolicy> {
112        match self {
113            StepConfig::Shell(c) => c.retry.as_ref(),
114            StepConfig::Http(c) => c.retry.as_ref(),
115            StepConfig::Agent(c) => c.retry.as_ref(),
116            StepConfig::Workflow(c) => c.retry.as_ref(),
117            StepConfig::Approval(_) | StepConfig::Decision(_) | StepConfig::Delay(_) => None,
118        }
119    }
120
121    /// Get the kind of step this configuration represents.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use ironflow_engine::config::{StepConfig, ShellConfig};
127    /// use ironflow_store::entities::StepKind;
128    ///
129    /// let config = StepConfig::Shell(ShellConfig::new("echo test"));
130    /// assert_eq!(config.kind(), StepKind::Shell);
131    /// ```
132    pub fn kind(&self) -> StepKind {
133        match self {
134            StepConfig::Shell(_) => StepKind::Shell,
135            StepConfig::Http(_) => StepKind::Http,
136            StepConfig::Agent(_) => StepKind::Agent,
137            StepConfig::Workflow(_) => StepKind::Workflow,
138            StepConfig::Approval(_) => StepKind::Approval,
139            StepConfig::Decision(_) => StepKind::Decision,
140            StepConfig::Delay(_) => StepKind::Custom("delay".to_string()),
141        }
142    }
143}
144
145impl From<ShellConfig> for StepConfig {
146    fn from(c: ShellConfig) -> Self {
147        StepConfig::Shell(c)
148    }
149}
150
151impl From<HttpConfig> for StepConfig {
152    fn from(c: HttpConfig) -> Self {
153        StepConfig::Http(c)
154    }
155}
156
157impl From<AgentStepConfig> for StepConfig {
158    fn from(c: AgentStepConfig) -> Self {
159        StepConfig::Agent(c)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use ironflow_core::retry::RetryPolicy;
166
167    use super::*;
168
169    #[test]
170    fn retry_accessor_returns_policy_for_each_variant() {
171        let shell = StepConfig::Shell(ShellConfig::new("echo").retry_policy(RetryPolicy::new(2)));
172        assert_eq!(shell.retry().unwrap().max_retries(), 2);
173
174        let http = StepConfig::Http(HttpConfig::get("http://x").retry_policy(RetryPolicy::new(3)));
175        assert_eq!(http.retry().unwrap().max_retries(), 3);
176
177        let workflow = StepConfig::Workflow(
178            WorkflowStepConfig::new("build", serde_json::json!({}))
179                .retry_policy(RetryPolicy::new(4)),
180        );
181        assert_eq!(workflow.retry().unwrap().max_retries(), 4);
182
183        let agent = StepConfig::Agent(AgentStepConfig::new("test"));
184        assert!(agent.retry().is_none());
185
186        let approval = StepConfig::Approval(ApprovalConfig::new("ok?"));
187        assert!(approval.retry().is_none());
188    }
189
190    #[test]
191    fn serde_roundtrip() {
192        let configs = vec![
193            StepConfig::Shell(ShellConfig::new("echo test")),
194            StepConfig::Http(HttpConfig::get("http://example.com")),
195            StepConfig::Agent(AgentStepConfig::new("summarize")),
196            StepConfig::Workflow(WorkflowStepConfig::new("build", serde_json::json!({}))),
197            StepConfig::Approval(ApprovalConfig::new("Deploy to production?")),
198            StepConfig::Delay(DelayConfig::from_secs(60)),
199        ];
200
201        for config in configs {
202            let json = serde_json::to_string(&config).expect("serialize");
203            let back: StepConfig = serde_json::from_str(&json).expect("deserialize");
204            let json2 = serde_json::to_string(&back).expect("serialize2");
205            assert_eq!(json, json2);
206        }
207    }
208
209    #[test]
210    fn approval_with_escalation_roundtrips() {
211        let config = StepConfig::Approval(
212            ApprovalConfig::new("Deploy to production?")
213                .with_deadline_secs(3600)
214                .assigned_to(Assignee::group("release-managers"))
215                .on_timeout(EscalationPolicy::Chain(vec![
216                    EscalationPolicy::Notify(vec![NotificationTarget::Webhook {
217                        url: "https://example.com/sla".to_string(),
218                    }]),
219                    EscalationPolicy::AutoReject,
220                ])),
221        );
222
223        let json = serde_json::to_string(&config).expect("serialize");
224        let back: StepConfig = serde_json::from_str(&json).expect("deserialize");
225        let json2 = serde_json::to_string(&back).expect("serialize2");
226        assert_eq!(json, json2);
227
228        let StepConfig::Approval(approval) = back else {
229            panic!("expected an approval config");
230        };
231        assert_eq!(approval.effective_deadline_secs(), Some(3600));
232        assert_eq!(
233            approval.assignee(),
234            Some(&Assignee::group("release-managers"))
235        );
236        assert_eq!(approval.effective_policy().len(), 2);
237    }
238}