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#[derive(Clone, Debug)]
17pub struct AutomationsSvc(pub(crate) Arc<Config>);
18
19impl AutomationsSvc {
20 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 let opts = UpdateAutomationOptions::new().with_status(AutomationStatus::Enabled);
601 let automation = resend.automations.update(&automation.id, opts).await?;
602
603 let automation = resend.automations.get(&automation.id).await?;
605
606 let automations = resend.automations.list(ListOptions::default()).await?;
608 assert!(!automations.data.is_empty());
609
610 let runs = resend
612 .automations
613 .list_runs(&automation.id, None, ListOptions::default())
614 .await?;
615 assert!(runs.data.is_empty());
616
617 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 let automation = resend.automations.stop(&automation.id).await?;
625
626 std::thread::sleep(std::time::Duration::from_secs(2));
627
628 let automation = resend.automations.delete(&automation.id).await?;
630 assert!(automation.deleted);
631
632 Ok(())
633 }
634}