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