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