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