ironflow_cli/commands/
schedule.rs1use anyhow::Result;
4use clap::{Args, Subcommand};
5use comfy_table::{ContentArrangement, Table};
6use ironflow_sdk::IronflowClient;
7use ironflow_sdk::types::{CreateScheduleRequest, ScheduleResponse};
8use uuid::Uuid;
9
10use crate::confirm::confirm;
11use crate::output;
12
13#[derive(Debug, Args)]
15pub struct ScheduleArgs {
16 #[command(subcommand)]
18 pub command: ScheduleCommands,
19}
20
21#[derive(Debug, Subcommand)]
23pub enum ScheduleCommands {
24 List,
26 Create {
28 workflow: String,
30 cron: String,
32 #[arg(long, default_value = "{}")]
34 inputs: String,
35 },
36 Pause {
38 id: Uuid,
40 },
41 Resume {
43 id: Uuid,
45 },
46 Delete {
48 id: Uuid,
50 #[arg(long)]
52 yes: bool,
53 },
54 Trigger {
56 id: Uuid,
58 },
59}
60
61fn schedule_state(s: &ScheduleResponse) -> String {
64 match (&s.disabled_at, &s.last_error) {
65 (None, _) => "active".to_string(),
66 (Some(_), Some(error)) => format!("disabled: {error}"),
67 (Some(_), None) => "paused".to_string(),
68 }
69}
70
71fn schedules_table(schedules: &[ScheduleResponse]) -> Table {
72 let mut table = Table::new();
73 table.set_content_arrangement(ContentArrangement::Dynamic);
74 table.set_header(vec![
75 "ID",
76 "WORKFLOW",
77 "CRON",
78 "SOURCE",
79 "ENABLED",
80 "NEXT TRIGGER",
81 ]);
82 for s in schedules {
83 let source = format!("{:?}", s.source).to_lowercase();
84 table.add_row(vec![
85 s.id.to_string(),
86 s.workflow_name.clone(),
87 s.cron_expression.clone(),
88 source.to_string(),
89 schedule_state(s),
90 s.next_trigger_at
91 .as_ref()
92 .map(|d| d.to_string())
93 .unwrap_or_else(|| "-".to_string()),
94 ]);
95 }
96 table
97}
98
99pub async fn execute(client: &IronflowClient, args: &ScheduleArgs, json_mode: bool) -> Result<()> {
106 match &args.command {
107 ScheduleCommands::List => {
108 let response = client.list_schedules().await?;
109 if json_mode {
110 output::print_json(&response)?;
111 } else {
112 println!("{}", schedules_table(&response.data));
113 }
114 Ok(())
115 }
116 ScheduleCommands::Create {
117 workflow,
118 cron,
119 inputs,
120 } => {
121 let parsed_inputs: serde_json::Value =
122 serde_json::from_str(inputs).map_err(|e| anyhow::anyhow!("invalid JSON: {e}"))?;
123 let response = client
124 .create_schedule(&CreateScheduleRequest {
125 workflow_name: workflow.clone(),
126 cron_expression: cron.clone(),
127 inputs: Some(parsed_inputs),
128 })
129 .await?;
130 if json_mode {
131 output::print_json(&response)?;
132 } else {
133 println!("Schedule {} created", response.data.id);
134 }
135 Ok(())
136 }
137 ScheduleCommands::Pause { id } => {
138 let response = client.pause_schedule(*id).await?;
139 if json_mode {
140 output::print_json(&response)?;
141 } else {
142 println!("Schedule {} paused", response.data.id);
143 }
144 Ok(())
145 }
146 ScheduleCommands::Resume { id } => {
147 let response = client.resume_schedule(*id).await?;
148 if json_mode {
149 output::print_json(&response)?;
150 } else {
151 println!("Schedule {} resumed", response.data.id);
152 }
153 Ok(())
154 }
155 ScheduleCommands::Delete { id, yes } => {
156 let prompt = format!("Delete schedule {id}?");
157 confirm(&prompt, *yes)?;
158 client.delete_schedule(*id).await?;
159 if json_mode {
160 output::print_json(&serde_json::json!({"deleted": id.to_string()}))?;
161 } else {
162 println!("Schedule {id} deleted");
163 }
164 Ok(())
165 }
166 ScheduleCommands::Trigger { id } => {
167 let response = client.trigger_schedule(*id).await?;
168 if json_mode {
169 output::print_json(&response)?;
170 } else {
171 println!("Schedule {} triggered", response.data.id);
172 }
173 Ok(())
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use serde_json::{from_value, json};
181
182 use super::*;
183
184 fn schedule(disabled_at: Option<&str>, last_error: Option<&str>) -> ScheduleResponse {
185 from_value(json!({
186 "id": "01a10fdb-f467-7982-826b-0c99470c7264",
187 "workflow_name": "deploy",
188 "cron_expression": "0 0 30 2 *",
189 "inputs": {},
190 "source": "api",
191 "disabled_at": disabled_at,
192 "last_triggered_at": null,
193 "next_trigger_at": null,
194 "last_error": last_error,
195 "created_by_user_id": null,
196 "created_at": "2026-10-06T08:00:00Z",
197 "updated_at": "2026-10-06T08:00:00Z"
198 }))
199 .unwrap()
200 }
201
202 #[test]
203 fn schedule_state_shows_why_ironflow_disabled_a_schedule() {
204 let s = schedule(
205 Some("2026-10-06T08:00:00Z"),
206 Some("cannot compute next trigger"),
207 );
208 assert_eq!(schedule_state(&s), "disabled: cannot compute next trigger");
209 assert!(
210 schedules_table(&[s])
211 .to_string()
212 .contains("disabled: cannot compute next trigger")
213 );
214 }
215
216 #[test]
217 fn schedule_state_tells_a_user_pause_from_an_active_schedule() {
218 assert_eq!(
219 schedule_state(&schedule(Some("2026-10-06T08:00:00Z"), None)),
220 "paused"
221 );
222 assert_eq!(schedule_state(&schedule(None, None)), "active");
223 }
224}