Skip to main content

ironflow_api/entities/
run.rs

1//! Run-related DTOs and query parameters.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use ironflow_store::models::{ConcurrencyLimit, ProviderKind, Run, RunStatus, TriggerKind};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12use super::{CreatedBy, StepResponse};
13
14/// Run response DTO — public API representation of a run.
15///
16/// Maps from the internal [`Run`] model, exposing only necessary fields.
17///
18/// # Examples
19///
20/// ```
21/// use ironflow_store::models::{Run, RunStatus, TriggerKind};
22/// use ironflow_api::entities::RunResponse;
23/// ```
24#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
25#[derive(Debug, Serialize, Deserialize)]
26pub struct RunResponse {
27    /// Unique run identifier.
28    pub id: Uuid,
29    /// Workflow name.
30    pub workflow_name: String,
31    /// Current status.
32    pub status: RunStatus,
33    /// How the run was triggered.
34    pub trigger: TriggerKind,
35    /// Optional error message.
36    pub error: Option<String>,
37    /// Number of times retried.
38    pub retry_count: u32,
39    /// Maximum allowed retries.
40    pub max_retries: u32,
41    /// Aggregated cost in USD.
42    #[cfg_attr(feature = "openapi", schema(value_type = f64))]
43    pub cost_usd: Decimal,
44    /// Total duration in milliseconds.
45    pub duration_ms: u64,
46    /// When created.
47    pub created_at: DateTime<Utc>,
48    /// When last updated.
49    pub updated_at: DateTime<Utc>,
50    /// When execution started.
51    pub started_at: Option<DateTime<Utc>>,
52    /// When execution completed.
53    pub completed_at: Option<DateTime<Utc>>,
54    /// Version of the handler that created this run.
55    pub handler_version: Option<String>,
56    /// User-defined key-value labels.
57    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
58    pub labels: HashMap<String, String>,
59    /// Scheduled execution time. `None` means the run executed immediately.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub scheduled_at: Option<DateTime<Utc>>,
62    /// Provider kind the run is waiting capacity for, set only while the
63    /// run is `sleeping` because every targeted Provider Account is rate
64    /// limited. `scheduled_at` then holds when the run wakes.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
67    pub capacity_wait_kind: Option<ProviderKind>,
68    /// Who triggered the run. Always present.
69    pub created_by: CreatedBy,
70    /// Idempotency key that produced this run, when one was supplied.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub idempotency_key: Option<String>,
73    /// Exclusivity key held by this run until it reaches a terminal state,
74    /// when one was supplied.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub concurrency_key: Option<String>,
77    /// Queue priority, from -100 to 100. Workers pick the pending run with
78    /// the highest priority first, then the oldest among equal priorities.
79    /// `0` is the default.
80    #[serde(default)]
81    pub priority: i16,
82    /// Concurrency groups the run belongs to, with the limit it was created
83    /// with. Omitted when the run belongs to no group.
84    #[serde(default, skip_serializing_if = "Vec::is_empty")]
85    pub concurrency_limits: Vec<ConcurrencyLimit>,
86    /// Cumulative cost cap for this run, in USD. `None` means no cap.
87    #[cfg_attr(feature = "openapi", schema(value_type = Option<f64>))]
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub max_cost_usd: Option<Decimal>,
90    /// Tags a worker must carry to take this run, sorted. Empty when any
91    /// worker can take it.
92    #[serde(default)]
93    pub worker_tags: Vec<String>,
94    /// Typed output the handler set with `WorkflowContext::set_output`.
95    ///
96    /// Written when the run ends (completed, warning, failed or cancelled).
97    /// Omitted when the handler set no output.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub output: Option<Value>,
100    /// State a `paused` run returns to when it is resumed. Omitted in every
101    /// other state.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub resume_status: Option<RunStatus>,
104}
105
106impl From<Run> for RunResponse {
107    fn from(run: Run) -> Self {
108        let created_by = CreatedBy::from(&run);
109        RunResponse {
110            id: run.id,
111            workflow_name: run.workflow_name,
112            status: run.status.state,
113            trigger: run.trigger,
114            error: run.error,
115            retry_count: run.retry_count,
116            max_retries: run.max_retries,
117            cost_usd: run.cost_usd,
118            duration_ms: run.duration_ms,
119            created_at: run.created_at,
120            updated_at: run.updated_at,
121            started_at: run.started_at,
122            completed_at: run.completed_at,
123            handler_version: run.handler_version,
124            labels: run.labels,
125            scheduled_at: run.scheduled_at,
126            capacity_wait_kind: run.capacity_wait_kind,
127            created_by,
128            idempotency_key: run.idempotency_key,
129            concurrency_key: run.concurrency_key,
130            priority: run.priority,
131            concurrency_limits: run.concurrency_limits,
132            max_cost_usd: run.max_cost_usd,
133            worker_tags: run.worker_tags,
134            output: run.output,
135            resume_status: run.resume_status,
136        }
137    }
138}
139
140/// Run detail response — includes steps and payload.
141#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
142#[derive(Debug, Serialize)]
143pub struct RunDetailResponse {
144    /// The run.
145    pub run: RunResponse,
146    /// Associated steps, ordered by position.
147    pub steps: Vec<StepResponse>,
148    /// Input payload that triggered this run.
149    pub payload: serde_json::Value,
150    /// Sub-workflow runs below this run, at any depth, that are not finished:
151    /// cancelling the run cancels them too.
152    pub active_descendant_count: u64,
153    /// Workers seen recently that could take the run. Only filled while the
154    /// run waits in the queue (`pending` or `retrying`), so a run no worker
155    /// can take is visible instead of waiting in silence.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub worker_routing: Option<WorkerRouting>,
158}
159
160/// Workers the API saw recently, as seen from one queued run.
161///
162/// Counted from the pick requests received in the last few minutes by this
163/// API process: with several replicas, each one only counts the workers that
164/// polled it.
165///
166/// # Examples
167///
168/// ```
169/// use ironflow_api::entities::WorkerRouting;
170///
171/// let routing = WorkerRouting {
172///     seen_workers: 2,
173///     eligible_workers: 0,
174/// };
175/// assert!(routing.eligible_workers < routing.seen_workers);
176/// ```
177#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179pub struct WorkerRouting {
180    /// Workers that asked for a run recently.
181    pub seen_workers: u32,
182    /// Among them, the workers that registered the run's workflow and carry
183    /// every tag it requires.
184    pub eligible_workers: u32,
185}
186
187/// Response of `POST /api/v1/runs/:id/cancel`: the cancelled run, with the
188/// sub-workflow runs cancelled along with it.
189///
190/// The run's fields stay at the top level, as before the descendants were
191/// listed, so existing clients read the same document.
192#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
193#[derive(Debug, Serialize)]
194pub struct CancelRunResponse {
195    /// The cancelled run.
196    #[serde(flatten)]
197    pub run: RunResponse,
198    /// Sub-workflow runs below it that this request cancelled, oldest first.
199    /// Empty when none was still active.
200    pub cancelled_descendants: Vec<Uuid>,
201}
202
203/// Response of `POST /api/v1/runs/:id/pause`: the paused run, with the
204/// sub-workflow runs paused along with it.
205#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
206#[derive(Debug, Serialize)]
207pub struct PauseRunResponse {
208    /// The paused run. Its `resume_status` is the state it returns to.
209    #[serde(flatten)]
210    pub run: RunResponse,
211    /// Sub-workflow runs below it that this request paused, oldest first.
212    pub paused_descendants: Vec<Uuid>,
213}
214
215/// Response of `POST /api/v1/runs/:id/resume`: the resumed run, with the
216/// sub-workflow runs resumed along with it.
217#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
218#[derive(Debug, Serialize)]
219pub struct ResumeRunResponse {
220    /// The resumed run, in the state it was paused from.
221    #[serde(flatten)]
222    pub run: RunResponse,
223    /// Sub-workflow runs below it that this request resumed, oldest first.
224    pub resumed_descendants: Vec<Uuid>,
225}
226
227/// Query parameters for listing runs.
228#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams, utoipa::ToSchema))]
229#[derive(Debug, Deserialize)]
230pub struct ListRunsQuery {
231    /// Filter by workflow name.
232    pub workflow: Option<String>,
233    /// Filter by run status.
234    pub status: Option<RunStatus>,
235    /// Filter by step presence (only applies to completed/cancelled runs).
236    /// Non-terminal runs (pending, running, etc.) are always included.
237    /// When `true`, only return completed/cancelled runs that have steps.
238    /// When `false`, only return completed/cancelled runs without steps.
239    pub has_steps: Option<bool>,
240    /// Filter by labels. Comma-separated `key:value` pairs.
241    pub label: Option<String>,
242    /// Filter by author: the user ID that triggered the run.
243    ///
244    /// Also matches runs triggered by one of that user's API keys.
245    pub created_by: Option<Uuid>,
246    /// Filter by concurrency group: only runs that belong to this group.
247    pub concurrency_group: Option<String>,
248    /// Filter by priority: only runs with exactly this priority.
249    pub priority: Option<i16>,
250    /// Page number (1-based).
251    pub page: Option<u32>,
252    /// Items per page.
253    pub per_page: Option<u32>,
254}
255
256impl ListRunsQuery {
257    /// Parse the comma-separated `label` param into a `HashMap`.
258    pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
259        parse_label_param(&self.label)
260    }
261}
262
263/// Parse a comma-separated `key:value` label query param into a `HashMap`.
264///
265/// Entries without a `:` are ignored. Returns `None` when the param is
266/// absent or contains no valid entry.
267pub(crate) fn parse_label_param(raw: &Option<String>) -> Option<HashMap<String, String>> {
268    raw.as_ref().and_then(|raw| {
269        let mut map = HashMap::new();
270        for entry in raw.split(',') {
271            let entry = entry.trim();
272            if let Some((k, v)) = entry.split_once(':') {
273                map.insert(k.to_string(), v.to_string());
274            }
275        }
276        if map.is_empty() { None } else { Some(map) }
277    })
278}