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