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