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