Skip to main content

ironflow_store/entities/
run.rs

1//! [`Run`] entity and related request/update types.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use uuid::Uuid;
10
11use super::{FsmState, RunStatus, TriggerKind};
12
13/// A workflow execution record.
14///
15/// Represents a single invocation of a workflow, tracking its status through
16/// the [`RunStatus`] FSM (SQL-side via [`lib_fsm`](crate::postgres::helpers::lib_fsm)),
17/// aggregated metrics, and timestamps.
18///
19/// # Examples
20///
21/// ```
22/// use ironflow_store::entities::Run;
23///
24/// // Runs are created by RunStore::create_run, not directly.
25/// ```
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[non_exhaustive]
28pub struct Run {
29    /// Unique identifier (UUIDv7, sortable by creation time).
30    pub id: Uuid,
31    /// Name of the workflow that was executed.
32    pub workflow_name: String,
33    /// Current FSM status — embeds state + state_machine_id for SQL-side transitions.
34    pub status: FsmState<RunStatus>,
35    /// How this run was triggered.
36    pub trigger: TriggerKind,
37    /// Trigger-specific payload (e.g. webhook body).
38    pub payload: Value,
39    /// Error message if the run failed.
40    pub error: Option<String>,
41    /// Number of times this run has been retried.
42    pub retry_count: u32,
43    /// Maximum number of retries allowed.
44    pub max_retries: u32,
45    /// Aggregated cost across all agent steps, in USD.
46    pub cost_usd: Decimal,
47    /// Aggregated wall-clock duration across all steps, in milliseconds.
48    pub duration_ms: u64,
49    /// When the run was created (enqueued).
50    pub created_at: DateTime<Utc>,
51    /// When the run record was last updated.
52    pub updated_at: DateTime<Utc>,
53    /// When execution started (transitioned to Running).
54    pub started_at: Option<DateTime<Utc>>,
55    /// When execution finished (transitioned to a terminal state).
56    pub completed_at: Option<DateTime<Utc>>,
57    /// Version of the handler that created this run.
58    pub handler_version: Option<String>,
59    /// User-defined key-value labels for categorization and filtering.
60    #[serde(default)]
61    pub labels: HashMap<String, String>,
62    /// When the run should start executing. `None` means immediately.
63    #[serde(default)]
64    pub scheduled_at: Option<DateTime<Utc>>,
65}
66
67/// Request to create a new run.
68///
69/// # Examples
70///
71/// ```
72/// use std::collections::HashMap;
73/// use ironflow_store::entities::{NewRun, TriggerKind};
74/// use serde_json::json;
75///
76/// let req = NewRun {
77///     workflow_name: "deploy".to_string(),
78///     trigger: TriggerKind::Manual,
79///     payload: json!({}),
80///     max_retries: 3,
81///     handler_version: None,
82///     labels: HashMap::new(),
83///     scheduled_at: None,
84/// };
85/// ```
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct NewRun {
88    /// Workflow name.
89    pub workflow_name: String,
90    /// How the run was triggered.
91    pub trigger: TriggerKind,
92    /// Trigger-specific payload.
93    pub payload: Value,
94    /// Maximum retry attempts.
95    pub max_retries: u32,
96    /// Version of the handler at the time of run creation.
97    pub handler_version: Option<String>,
98    /// User-defined key-value labels for categorization and filtering.
99    #[serde(default)]
100    pub labels: HashMap<String, String>,
101    /// When the run should start executing. `None` means immediately.
102    #[serde(default)]
103    pub scheduled_at: Option<DateTime<Utc>>,
104}
105
106/// Filters for listing runs.
107///
108/// All fields are optional; `None` means "no filter" for that field.
109///
110/// # Examples
111///
112/// ```
113/// use ironflow_store::entities::{RunFilter, RunStatus};
114///
115/// let filter = RunFilter {
116///     workflow_name: Some("deploy".to_string()),
117///     status: Some(RunStatus::Completed),
118///     ..RunFilter::default()
119/// };
120/// ```
121#[derive(Debug, Clone, Default)]
122pub struct RunFilter {
123    /// Filter by workflow name (exact match).
124    pub workflow_name: Option<String>,
125    /// Filter by run status.
126    pub status: Option<RunStatus>,
127    /// Only include runs created after this timestamp.
128    pub created_after: Option<DateTime<Utc>>,
129    /// Only include runs created before this timestamp.
130    pub created_before: Option<DateTime<Utc>>,
131    /// When `Some(true)`, only include runs that have at least one step.
132    /// When `Some(false)`, only include runs with no steps.
133    /// When `None`, no filtering on steps.
134    pub has_steps: Option<bool>,
135    /// Filter by label key-value pair. Only include runs that have ALL specified labels.
136    pub labels: Option<HashMap<String, String>>,
137}
138
139/// Partial update for a run.
140///
141/// # Examples
142///
143/// ```
144/// use ironflow_store::entities::{RunUpdate, RunStatus};
145///
146/// let update = RunUpdate {
147///     status: Some(RunStatus::Completed),
148///     ..RunUpdate::default()
149/// };
150/// ```
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152pub struct RunUpdate {
153    /// New status.
154    pub status: Option<RunStatus>,
155    /// Error message.
156    pub error: Option<String>,
157    /// Increment retry count.
158    pub increment_retry: bool,
159    /// Aggregated cost.
160    pub cost_usd: Option<Decimal>,
161    /// Aggregated duration.
162    pub duration_ms: Option<u64>,
163    /// When execution started.
164    pub started_at: Option<DateTime<Utc>>,
165    /// When execution completed.
166    pub completed_at: Option<DateTime<Utc>>,
167}
168
169#[cfg(test)]
170mod tests {
171    use std::collections::HashMap;
172
173    use super::*;
174    use serde_json::json;
175
176    #[test]
177    fn newrun_serde_roundtrip() {
178        let new_run = NewRun {
179            workflow_name: "deploy".to_string(),
180            trigger: TriggerKind::Manual,
181            payload: json!({"key": "value"}),
182            max_retries: 3,
183            handler_version: Some("1.2.0".to_string()),
184            labels: HashMap::from([("env".to_string(), "prod".to_string())]),
185            scheduled_at: None,
186        };
187
188        let json = serde_json::to_string(&new_run).expect("serialize");
189        let back: NewRun = serde_json::from_str(&json).expect("deserialize");
190        assert_eq!(back.workflow_name, new_run.workflow_name);
191        assert_eq!(back.trigger, new_run.trigger);
192        assert_eq!(back.payload, new_run.payload);
193        assert_eq!(back.max_retries, new_run.max_retries);
194        assert_eq!(back.handler_version, new_run.handler_version);
195        assert_eq!(back.labels, new_run.labels);
196        assert_eq!(back.scheduled_at, new_run.scheduled_at);
197    }
198
199    #[test]
200    fn run_serde_preserves_all_fields() {
201        use crate::entities::FsmState;
202        use chrono::Utc;
203        use uuid::Uuid;
204
205        let now = Utc::now();
206        let run = Run {
207            id: Uuid::now_v7(),
208            workflow_name: "test-wf".to_string(),
209            status: FsmState::new(RunStatus::Running, Uuid::now_v7()),
210            trigger: TriggerKind::Webhook {
211                path: "/hooks/test".to_string(),
212            },
213            payload: json!({"data": 123}),
214            error: Some("test error".to_string()),
215            retry_count: 2,
216            max_retries: 5,
217            cost_usd: Decimal::new(1234, 2),
218            duration_ms: 5000,
219            created_at: now,
220            updated_at: now,
221            started_at: Some(now),
222            completed_at: Some(now),
223            handler_version: Some("2.0.0".to_string()),
224            labels: HashMap::from([
225                ("env".to_string(), "staging".to_string()),
226                ("team".to_string(), "platform".to_string()),
227            ]),
228            scheduled_at: Some(now),
229        };
230
231        let json = serde_json::to_string(&run).expect("serialize");
232        let back: Run = serde_json::from_str(&json).expect("deserialize");
233
234        assert_eq!(back.id, run.id);
235        assert_eq!(back.workflow_name, run.workflow_name);
236        assert_eq!(back.status.state, run.status.state);
237        assert_eq!(back.trigger, run.trigger);
238        assert_eq!(back.payload, run.payload);
239        assert_eq!(back.error, run.error);
240        assert_eq!(back.retry_count, run.retry_count);
241        assert_eq!(back.max_retries, run.max_retries);
242        assert_eq!(back.cost_usd, run.cost_usd);
243        assert_eq!(back.duration_ms, run.duration_ms);
244        assert_eq!(back.started_at, run.started_at);
245        assert_eq!(back.completed_at, run.completed_at);
246        assert_eq!(back.handler_version, run.handler_version);
247        assert_eq!(back.labels, run.labels);
248        assert_eq!(back.scheduled_at, run.scheduled_at);
249    }
250
251    #[test]
252    fn runupdate_serde_roundtrip() {
253        let update = RunUpdate {
254            status: Some(RunStatus::Completed),
255            error: Some("test error".to_string()),
256            increment_retry: true,
257            cost_usd: Some(Decimal::new(5000, 2)),
258            duration_ms: Some(3000),
259            started_at: None,
260            completed_at: None,
261        };
262
263        let json = serde_json::to_string(&update).expect("serialize");
264        let back: RunUpdate = serde_json::from_str(&json).expect("deserialize");
265
266        assert_eq!(back.status, update.status);
267        assert_eq!(back.error, update.error);
268        assert_eq!(back.increment_retry, update.increment_retry);
269        assert_eq!(back.cost_usd, update.cost_usd);
270        assert_eq!(back.duration_ms, update.duration_ms);
271    }
272
273    #[test]
274    fn runfilter_default_is_no_filters() {
275        let filter = RunFilter::default();
276        assert!(filter.workflow_name.is_none());
277        assert!(filter.status.is_none());
278        assert!(filter.created_after.is_none());
279        assert!(filter.created_before.is_none());
280    }
281
282    #[test]
283    fn runfilter_with_multiple_criteria() {
284        let filter = RunFilter {
285            workflow_name: Some("deploy".to_string()),
286            status: Some(RunStatus::Running),
287            ..RunFilter::default()
288        };
289
290        assert_eq!(filter.workflow_name, Some("deploy".to_string()));
291        assert_eq!(filter.status, Some(RunStatus::Running));
292        assert!(filter.created_after.is_none());
293        assert!(filter.created_before.is_none());
294    }
295}