Skip to main content

resend_rs/
automations.rs

1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6    Config, Result,
7    list_opts::{ListOptions, ListResponse},
8    types::{
9        Automation, AutomationMinimal, AutomationRun, CreateAutomationOptions,
10        CreateAutomationResponse, DeleteAutomationResponse, DuplicateAutomationResponse,
11        StopAutomationResponse, UpdateAutomationOptions, UpdateAutomationResponse,
12    },
13};
14
15/// `Resend` APIs for `/automations` endpoints.
16#[derive(Clone, Debug)]
17pub struct AutomationsSvc(pub(crate) Arc<Config>);
18
19impl AutomationsSvc {
20    /// Create a new automation to automate email sequences.
21    ///
22    /// <https://resend.com/docs/api-reference/automations/create-automation>
23    #[maybe_async::maybe_async]
24    pub async fn create(
25        &self,
26        automation: CreateAutomationOptions,
27    ) -> Result<CreateAutomationResponse> {
28        let request = self.0.build(Method::POST, "/automations");
29        let response = self.0.send(request.json(&automation)).await?;
30        let content = response.json::<CreateAutomationResponse>().await?;
31
32        Ok(content)
33    }
34
35    /// Update an existing automation.
36    ///
37    /// <https://resend.com/docs/api-reference/automations/update-automation>
38    #[maybe_async::maybe_async]
39    pub async fn update(
40        &self,
41        automation_id: &str,
42        update: UpdateAutomationOptions,
43    ) -> Result<UpdateAutomationResponse> {
44        let path = format!("/automations/{automation_id}");
45
46        let request = self.0.build(Method::PATCH, &path);
47        let response = self.0.send(request.json(&update)).await?;
48        let content = response.json::<UpdateAutomationResponse>().await?;
49
50        Ok(content)
51    }
52
53    /// Retrieve a single automation.
54    ///
55    /// <https://resend.com/docs/api-reference/automations/get-automation>
56    #[maybe_async::maybe_async]
57    pub async fn get(&self, automation_id: &str) -> Result<Automation> {
58        let path = format!("/automations/{automation_id}");
59
60        let request = self.0.build(Method::GET, &path);
61        let response = self.0.send(request).await?;
62        let content = response.json::<Automation>().await?;
63
64        Ok(content)
65    }
66
67    /// Retrieve a list of automations.
68    ///
69    /// <https://resend.com/docs/api-reference/automations/list-automations>
70    #[maybe_async::maybe_async]
71    pub async fn list<T>(
72        &self,
73        list_opts: ListOptions<T>,
74    ) -> Result<ListResponse<AutomationMinimal>> {
75        let request = self.0.build(Method::GET, "/automations").query(&list_opts);
76        let response = self.0.send(request).await?;
77        let content = response.json::<ListResponse<AutomationMinimal>>().await?;
78
79        Ok(content)
80    }
81
82    /// Stop a running automation.
83    ///
84    /// <https://resend.com/docs/api-reference/automations/stop-automation>
85    #[maybe_async::maybe_async]
86    pub async fn stop(&self, automation_id: &str) -> Result<StopAutomationResponse> {
87        let path = format!("/automations/{automation_id}/stop");
88
89        let request = self.0.build(Method::POST, &path);
90        let response = self.0.send(request).await?;
91        let content = response.json::<StopAutomationResponse>().await?;
92
93        Ok(content)
94    }
95
96    /// Duplicate an existing automation.
97    ///
98    /// <https://resend.com/docs/api-reference/automations/duplicate-automation>
99    #[maybe_async::maybe_async]
100    pub async fn duplicate(&self, automation_id: &str) -> Result<DuplicateAutomationResponse> {
101        let path = format!("/automations/{automation_id}/duplicate");
102
103        let request = self.0.build(Method::POST, &path);
104        let response = self.0.send(request).await?;
105        let content = response.json::<DuplicateAutomationResponse>().await?;
106
107        Ok(content)
108    }
109
110    /// Remove an existing automation.
111    ///
112    /// <https://resend.com/docs/api-reference/automations/delete-automation>
113    #[maybe_async::maybe_async]
114    pub async fn delete(&self, automation_id: &str) -> Result<DeleteAutomationResponse> {
115        let path = format!("/automations/{automation_id}");
116
117        let request = self.0.build(Method::DELETE, &path);
118        let response = self.0.send(request).await?;
119        let content = response.json::<DeleteAutomationResponse>().await?;
120
121        Ok(content)
122    }
123
124    /// Retrieve a list of automation runs.
125    ///
126    /// <https://resend.com/docs/api-reference/automations/list-automation-runs>
127    #[maybe_async::maybe_async]
128    pub async fn list_runs<T>(
129        &self,
130        automation_id: &str,
131        status_filter: Option<String>,
132        list_opts: ListOptions<T>,
133    ) -> Result<ListResponse<AutomationRun>> {
134        let path = format!("/automations/{automation_id}/runs");
135
136        let request = self
137            .0
138            .build(Method::GET, &path)
139            .query(&list_opts)
140            .query(&status_filter);
141        let response = self.0.send(request).await?;
142        let content = response.json::<ListResponse<AutomationRun>>().await?;
143
144        Ok(content)
145    }
146
147    /// Retrieve a single automation run.
148    ///
149    /// <https://resend.com/docs/api-reference/automations/get-automation-run>
150    #[maybe_async::maybe_async]
151    pub async fn get_run(&self, automation_id: &str, run_id: &str) -> Result<AutomationRun> {
152        let path = format!("/automations/{automation_id}/runs/{run_id}");
153
154        let request = self.0.build(Method::GET, &path);
155        let response = self.0.send(request).await?;
156        let content = response.json::<AutomationRun>().await?;
157
158        Ok(content)
159    }
160}
161
162#[allow(unreachable_pub)]
163pub mod types {
164    use std::collections::HashMap;
165
166    use serde::{Deserialize, Serialize};
167    use serde_json::Value;
168
169    crate::define_id_type!(AutomationId);
170    crate::define_id_type!(AutomationRunId);
171
172    #[must_use]
173    #[derive(Debug, Clone, Serialize)]
174    pub struct CreateAutomationOptions {
175        pub name: String,
176        pub status: AutomationStatus,
177        pub steps: Vec<Step>,
178        pub connections: Vec<Connection>,
179    }
180
181    #[must_use]
182    #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
183    #[serde(rename_all = "snake_case")]
184    pub enum AutomationStatus {
185        Enabled,
186        #[default]
187        Disabled,
188    }
189
190    #[must_use]
191    #[derive(Debug, Clone, Serialize, Deserialize)]
192    #[serde(tag = "type", rename_all = "snake_case")]
193    pub enum Step {
194        Trigger {
195            key: String,
196            config: TriggerStepConfig,
197        },
198        SendEmail {
199            key: String,
200            config: SendEmailStepConfig,
201        },
202        Delay {
203            key: String,
204            config: DelayStepConfig,
205        },
206        WaitForEvent {
207            key: String,
208            config: WaitForEventStepConfig,
209        },
210        Condition {
211            key: String,
212            config: Value,
213        },
214        ContactUpdate {
215            key: String,
216            config: Value,
217        },
218        ContactDelete {
219            key: String,
220            config: Value,
221        },
222        AddToSegment {
223            key: String,
224            config: AddToSegmentStepConfig,
225        },
226    }
227
228    #[must_use]
229    #[derive(Debug, Clone, Serialize, Deserialize)]
230    pub struct TriggerStepConfig {
231        pub event_name: String,
232    }
233
234    #[must_use]
235    #[derive(Debug, Clone, Serialize, Deserialize)]
236    pub struct SendEmailStepConfig {
237        pub template: AutomationTemplate,
238        pub subject: Option<String>,
239        pub from: Option<String>,
240        pub reply_to: Option<String>,
241        pub variables: Option<Value>,
242    }
243
244    impl SendEmailStepConfig {
245        #[inline]
246        pub fn new(template: AutomationTemplate) -> Self {
247            Self {
248                template,
249                subject: None,
250                from: None,
251                reply_to: None,
252                variables: None,
253            }
254        }
255
256        #[inline]
257        pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
258            self.subject = Some(subject.into());
259            self
260        }
261
262        #[inline]
263        pub fn with_from(mut self, from: impl Into<String>) -> Self {
264            self.from = Some(from.into());
265            self
266        }
267
268        #[inline]
269        pub fn with_reply_to(mut self, reply_to: impl Into<String>) -> Self {
270            self.reply_to = Some(reply_to.into());
271            self
272        }
273
274        #[inline]
275        pub fn with_variables(mut self, variables: Value) -> Self {
276            self.variables = Some(variables);
277            self
278        }
279    }
280
281    #[must_use]
282    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
283    pub struct AutomationTemplate {
284        pub id: String,
285        #[serde(skip_serializing_if = "Option::is_none")]
286        pub variables: Option<Value>,
287    }
288
289    impl AutomationTemplate {
290        #[inline]
291        pub fn new(id: impl Into<String>) -> Self {
292            Self {
293                id: id.into(),
294                variables: None,
295            }
296        }
297
298        #[inline]
299        pub fn with_variables(mut self, variables: Value) -> Self {
300            self.variables = Some(variables);
301            self
302        }
303    }
304
305    #[must_use]
306    #[derive(Debug, Clone, Serialize, Deserialize)]
307    pub struct DelayStepConfig {
308        pub duration: String,
309    }
310
311    #[must_use]
312    #[derive(Debug, Clone, Serialize, Deserialize)]
313    pub struct WaitForEventStepConfig {
314        pub event_name: String,
315        pub timeout: Option<String>,
316        pub filter_rule: Option<Value>,
317    }
318
319    #[must_use]
320    #[derive(Debug, Clone, Serialize, Deserialize)]
321    pub struct AddToSegmentStepConfig {
322        pub segment_id: String,
323    }
324
325    #[must_use]
326    #[derive(Debug, Clone, Serialize, Deserialize)]
327    pub struct Connection {
328        pub from: String,
329        pub to: String,
330        pub r#type: Option<ConnectionType>,
331    }
332
333    impl Connection {
334        #[inline]
335        pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
336            Self {
337                from: from.into(),
338                to: to.into(),
339                r#type: None,
340            }
341        }
342
343        #[inline]
344        pub fn with_type(mut self, r#type: ConnectionType) -> Self {
345            self.r#type = Some(r#type);
346            self
347        }
348    }
349
350    #[must_use]
351    #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
352    #[serde(rename_all = "snake_case")]
353    pub enum ConnectionType {
354        #[default]
355        Default,
356        ConditionMet,
357        ConditionNotMet,
358        Timeout,
359        EventReceived,
360    }
361
362    #[derive(Debug, Clone, Serialize, Deserialize)]
363    pub struct CreateAutomationResponse {
364        pub id: AutomationId,
365    }
366
367    #[must_use]
368    #[derive(Debug, Clone, Serialize, Deserialize)]
369    pub struct Automation {
370        pub id: AutomationId,
371        pub name: String,
372        pub status: AutomationStatus,
373        pub created_at: String,
374        pub updated_at: Option<String>,
375        pub steps: Vec<Step>,
376        pub connections: Vec<Connection>,
377    }
378
379    #[must_use]
380    #[derive(Debug, Clone, Serialize, Deserialize)]
381    pub struct AutomationMinimal {
382        pub id: AutomationId,
383        pub name: String,
384        pub status: AutomationStatus,
385        pub created_at: String,
386        pub updated_at: Option<String>,
387    }
388
389    #[must_use]
390    #[derive(Debug, Clone, Serialize, Default)]
391    pub struct UpdateAutomationOptions {
392        #[serde(skip_serializing_if = "Option::is_none")]
393        name: Option<String>,
394        #[serde(skip_serializing_if = "Option::is_none")]
395        status: Option<AutomationStatus>,
396        #[serde(skip_serializing_if = "Option::is_none")]
397        steps: Option<Vec<Step>>,
398        #[serde(skip_serializing_if = "Option::is_none")]
399        connections: Option<Vec<Connection>>,
400    }
401
402    impl UpdateAutomationOptions {
403        #[inline]
404        pub fn new() -> Self {
405            Self::default()
406        }
407
408        #[inline]
409        pub fn with_name(mut self, name: &str) -> Self {
410            self.name = Some(name.to_owned());
411            self
412        }
413
414        #[inline]
415        pub fn with_status(mut self, status: AutomationStatus) -> Self {
416            self.status = Some(status);
417            self
418        }
419
420        #[inline]
421        pub fn with_steps(mut self, steps: Vec<Step>) -> Self {
422            self.steps = Some(steps);
423            self
424        }
425
426        #[inline]
427        pub fn with_connections(mut self, connections: Vec<Connection>) -> Self {
428            self.connections = Some(connections);
429            self
430        }
431    }
432
433    #[must_use]
434    #[derive(Debug, Clone, Serialize, Deserialize)]
435    pub struct UpdateAutomationResponse {
436        pub id: AutomationId,
437    }
438
439    #[must_use]
440    #[derive(Debug, Clone, Serialize, Deserialize)]
441    pub struct StopAutomationResponse {
442        pub id: AutomationId,
443        pub status: AutomationStatus,
444    }
445
446    #[must_use]
447    #[derive(Debug, Clone, Serialize, Deserialize)]
448    pub struct DuplicateAutomationResponse {
449        pub id: AutomationId,
450    }
451
452    #[derive(Debug, Clone, Serialize, Deserialize)]
453    pub struct DeleteAutomationResponse {
454        pub id: AutomationId,
455        pub deleted: bool,
456    }
457
458    #[must_use]
459    #[derive(Debug, Clone, Serialize, Deserialize)]
460    pub struct AutomationRun {
461        id: AutomationRunId,
462        #[serde(skip_serializing_if = "Option::is_none")]
463        started_at: Option<String>,
464        #[serde(skip_serializing_if = "Option::is_none")]
465        completed_at: Option<String>,
466        created_at: String,
467        status: AutomationRunStatus,
468        trigger: Option<AutomationRunTrigger>,
469    }
470
471    #[must_use]
472    #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
473    #[serde(rename_all = "snake_case")]
474    pub enum AutomationRunStatus {
475        Running,
476        Completed,
477        Failed,
478        Cancelled,
479    }
480
481    #[must_use]
482    #[derive(Debug, Clone, Serialize, Deserialize)]
483    pub struct AutomationRunTrigger {
484        event_name: String,
485        #[serde(skip_serializing_if = "Option::is_none")]
486        payload: Option<HashMap<String, Value>>,
487    }
488}
489
490#[cfg(test)]
491#[allow(clippy::unwrap_used)]
492#[allow(clippy::needless_return)]
493mod test {
494    use crate::{
495        automations::types::{SendEmailStepConfig, TriggerStepConfig},
496        types::{Automation, AutomationStatus, Connection, CreateAutomationOptions, Step},
497    };
498
499    #[cfg(not(feature = "blocking"))]
500    use crate::{
501        list_opts::ListOptions,
502        test::{CLIENT, DebugResult},
503        types::UpdateAutomationOptions,
504    };
505
506    #[test]
507    fn serialize_create() {
508        let tmp = CreateAutomationOptions {
509            name: "Welcome series".to_owned(),
510            status: AutomationStatus::Enabled,
511            steps: vec![
512                Step::Trigger {
513                    key: "start".to_owned(),
514                    config: TriggerStepConfig {
515                        event_name: "user.created".to_owned(),
516                    },
517                },
518                Step::SendEmail {
519                    key: "welcome".to_owned(),
520                    config: SendEmailStepConfig {
521                        subject: None,
522                        from: None,
523                        reply_to: None,
524                        variables: None,
525                        template: crate::automations::types::AutomationTemplate {
526                            id: "34a080c9-b17d-4187-ad80-5af20266e535".to_owned(),
527                            variables: None,
528                        },
529                    },
530                },
531            ],
532            connections: vec![Connection {
533                from: "start".to_owned(),
534                to: "welcome".to_owned(),
535                r#type: None,
536            }],
537        };
538
539        println!("{}", serde_json::to_string(&tmp).unwrap());
540    }
541
542    #[test]
543    fn deserialize_get() {
544        let tmp = r#"
545          {
546            "object": "automation",
547            "id": "c9b16d4f-ba6c-4e2e-b044-6bf4404e57fd",
548            "name": "Welcome series",
549            "status": "disabled",
550            "created_at": "2026-10-01 12:00:00.000000+00",
551            "updated_at": "2026-10-01 12:00:00.000000+00",
552            "steps": [
553              {
554                "key": "start",
555                "type": "trigger",
556                "config": { "event_name": "user.created" }
557              },
558              {
559                "key": "welcome",
560                "type": "send_email",
561                "config": {
562                  "template": { "id": "34a080c9-b17d-4187-ad80-5af20266e535" }
563                }
564              }
565            ],
566            "connections": [
567              {
568                "from": "start",
569                "to": "welcome",
570                "type": "default"
571              }
572            ]
573          }"#;
574
575        let _res = serde_json::from_str::<Automation>(tmp).unwrap();
576    }
577
578    #[tokio_shared_rt::test(shared = true)]
579    #[serial_test::serial]
580    #[cfg(not(feature = "blocking"))]
581    async fn all() -> DebugResult<()> {
582        let resend = &*CLIENT;
583
584        // Create
585        let opts = CreateAutomationOptions {
586            name: "Welcome series".to_owned(),
587            status: AutomationStatus::Enabled,
588            steps: vec![Step::Trigger {
589                key: "trigger".to_owned(),
590                config: TriggerStepConfig {
591                    event_name: "user.created".to_owned(),
592                },
593            }],
594            connections: vec![],
595        };
596        let automation = resend.automations.create(opts).await?;
597        std::thread::sleep(std::time::Duration::from_secs(2));
598
599        // Update
600        let opts = UpdateAutomationOptions::new().with_status(AutomationStatus::Enabled);
601        let automation = resend.automations.update(&automation.id, opts).await?;
602
603        // Get
604        let automation = resend.automations.get(&automation.id).await?;
605
606        // List
607        let automations = resend.automations.list(ListOptions::default()).await?;
608        assert!(!automations.data.is_empty());
609
610        // List Runs
611        let runs = resend
612            .automations
613            .list_runs(&automation.id, None, ListOptions::default())
614            .await?;
615        assert!(runs.data.is_empty());
616
617        // Duplicate
618        let duplicated = resend.automations.duplicate(&automation.id).await?;
619        std::thread::sleep(std::time::Duration::from_secs(2));
620        let deleted = resend.automations.delete(&duplicated.id).await?;
621        assert!(deleted.deleted);
622
623        // Stop
624        let automation = resend.automations.stop(&automation.id).await?;
625
626        std::thread::sleep(std::time::Duration::from_secs(2));
627
628        // Delete
629        let automation = resend.automations.delete(&automation.id).await?;
630        assert!(automation.deleted);
631
632        Ok(())
633    }
634}