loops_sdk 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26e2946fb1c882b535762d964ef73b0c79c7631315527f3b967aeaab5fe9cb13
4
- data.tar.gz: f44c32833e2191a6565541c87ed6242580e2cd5e01ca023f6b4444c706a6c6f0
3
+ metadata.gz: a8a4baf2050d5fbaeeb96e5c39000b77fb84603d539218df1b001763d265aeab
4
+ data.tar.gz: 707d8a389ae61d4f8c3c0621a6ec4edb4b0d1c822084e2b680b41ecf7f6d6375
5
5
  SHA512:
6
- metadata.gz: fcbedd17d573585d04d0e474fc5f2f2d712dd7c3da76fae62675df80d1277aafbcfc3ab9992cd764208e8a1ebd0295ee72a4c5b5bd4b9d522e1cca5ab53cdbd2
7
- data.tar.gz: 64048aaf770aa228250d012a6fd83747c0f62ec2f3c75e531426fd6982c882b4488bbea92a945ac4a2f8cfb6c8cae445d601dcb2bd96f61163c82ed90a5c15f9
6
+ metadata.gz: f0b31acb878a70e3665a081e26db46e61cd32cf4d4d032560547122f12c74ef5d0fd03824d481b981f836d9d2aa9ba4ccc279b3131a2dfab01dad3e148fa4c6a
7
+ data.tar.gz: 8c7575f7b5882b62bf7d627c36be1ac801f5f31b973c5e72a787c6b61247e5766219642dcc75eab860fbcba4ad9f0e9925e4479d1927c8b58c2d881e96515493
data/README.md CHANGED
@@ -120,6 +120,7 @@ You can use custom contact properties in API calls. Please make sure to [add cus
120
120
  - [Transactional.update()](#transactionalupdate)
121
121
  - [Transactional.ensure_draft()](#transactionalensure_draft)
122
122
  - [Transactional.publish()](#transactionalpublish)
123
+ - [Transactional.metrics()](#transactionalmetrics)
123
124
  - [Transactional.send()](#transactionalsend)
124
125
  - [DedicatedSendingIps.list()](#dedicatedsendingipslist)
125
126
  - [Themes.list()](#themeslist)
@@ -134,10 +135,15 @@ You can use custom contact properties in API calls. Please make sure to [add cus
134
135
  - [Campaigns.create()](#campaignscreate)
135
136
  - [Campaigns.get()](#campaignsget)
136
137
  - [Campaigns.update()](#campaignsupdate)
138
+ - [Campaigns.metrics()](#campaignsmetrics)
137
139
  - [CampaignGroups.list()](#campaigngroupslist)
138
140
  - [CampaignGroups.create()](#campaigngroupscreate)
139
141
  - [CampaignGroups.get()](#campaigngroupsget)
140
142
  - [CampaignGroups.update()](#campaigngroupsupdate)
143
+ - [WorkflowGroups.list()](#workflowgroupslist)
144
+ - [WorkflowGroups.create()](#workflowgroupscreate)
145
+ - [WorkflowGroups.get()](#workflowgroupsget)
146
+ - [WorkflowGroups.update()](#workflowgroupsupdate)
141
147
  - [AudienceSegments.list()](#audiencesegmentslist)
142
148
  - [AudienceSegments.get()](#audiencesegmentsget)
143
149
  - [AudienceSegments.create()](#audiencesegmentscreate)
@@ -145,12 +151,15 @@ You can use custom contact properties in API calls. Please make sure to [add cus
145
151
  - [Workflows.create()](#workflowscreate)
146
152
  - [Workflows.get()](#workflowsget)
147
153
  - [Workflows.update()](#workflowsupdate)
154
+ - [Workflows.delete()](#workflowsdelete)
148
155
  - [Workflows.change_mailing_list()](#workflowschange_mailing_list)
149
156
  - [Workflows.get_node()](#workflowsget_node)
157
+ - [Workflows.get_node_metrics()](#workflowsget_node_metrics)
150
158
  - [Workflows.create_node()](#workflowscreate_node)
151
159
  - [Workflows.update_node()](#workflowsupdate_node)
152
160
  - [Workflows.delete_node()](#workflowsdelete_node)
153
161
  - [Workflows.add_branch()](#workflowsadd_branch)
162
+ - [Workflows.reroute_node()](#workflowsreroute_node)
154
163
  - [Workflows.delete_node_recursive()](#workflowsdelete_node_recursive)
155
164
  - [EventPatterns.list()](#eventpatternslist)
156
165
  - [EventPatterns.get()](#eventpatternsget)
@@ -998,6 +1007,38 @@ response = LoopsSdk::Transactional.publish(transactional_id: "clfq6dinn000yl70fg
998
1007
 
999
1008
  ---
1000
1009
 
1010
+ ### Transactional.metrics()
1011
+
1012
+ Get all-time delivery metrics for a transactional email.
1013
+
1014
+ [API Reference](https://loops.so/docs/api-reference/get-transactional-email-metrics)
1015
+
1016
+ #### Parameters
1017
+
1018
+ | Name | Type | Required | Notes |
1019
+ | ------------------ | ------ | -------- | ----- |
1020
+ | `transactional_id` | string | Yes | |
1021
+
1022
+ #### Example
1023
+
1024
+ ```ruby
1025
+ response = LoopsSdk::Transactional.metrics(transactional_id: "clfq6dinn000yl70fgwwyp82l")
1026
+ ```
1027
+
1028
+ #### Response
1029
+
1030
+ ```json
1031
+ {
1032
+ "sends": 4210,
1033
+ "deliveries": 4175,
1034
+ "spamReports": 1,
1035
+ "hardBounces": 9,
1036
+ "softBounces": 13
1037
+ }
1038
+ ```
1039
+
1040
+ ---
1041
+
1001
1042
  ### Transactional.send()
1002
1043
 
1003
1044
  Send a transactional email to a contact. [Learn about sending transactional email](https://loops.so/docs/transactional/guide)
@@ -1612,6 +1653,40 @@ response = LoopsSdk::Campaigns.update(
1612
1653
 
1613
1654
  ---
1614
1655
 
1656
+ ### Campaigns.metrics()
1657
+
1658
+ Get all-time engagement metrics for a campaign.
1659
+
1660
+ [API Reference](https://loops.so/docs/api-reference/get-campaign-metrics)
1661
+
1662
+ #### Parameters
1663
+
1664
+ | Name | Type | Required | Notes |
1665
+ | ------------- | ------ | -------- | ----- |
1666
+ | `campaign_id` | string | Yes | |
1667
+
1668
+ #### Example
1669
+
1670
+ ```ruby
1671
+ response = LoopsSdk::Campaigns.metrics(campaign_id: "cln4o7p9q0110msw5ekjtmv78")
1672
+ ```
1673
+
1674
+ #### Response
1675
+
1676
+ ```json
1677
+ {
1678
+ "sends": 4210,
1679
+ "opens": 1922,
1680
+ "clicks": 301,
1681
+ "unsubscribes": 12,
1682
+ "spamReports": 1,
1683
+ "hardBounces": 9,
1684
+ "softBounces": 13
1685
+ }
1686
+ ```
1687
+
1688
+ ---
1689
+
1615
1690
  ### EmailMessages.get()
1616
1691
 
1617
1692
  Get an email message, including its LMX content.
@@ -1931,6 +2006,155 @@ response = LoopsSdk::CampaignGroups.update(
1931
2006
 
1932
2007
  ---
1933
2008
 
2009
+ ### WorkflowGroups.list()
2010
+
2011
+ List workflow groups.
2012
+
2013
+ [API Reference](https://loops.so/docs/api-reference/list-workflow-groups)
2014
+
2015
+ #### Parameters
2016
+
2017
+ | Name | Type | Required | Notes |
2018
+ | --------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
2019
+ | `perPage` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
2020
+ | `cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |
2021
+
2022
+ #### Example
2023
+
2024
+ ```ruby
2025
+ response = LoopsSdk::WorkflowGroups.list
2026
+ ```
2027
+
2028
+ #### Response
2029
+
2030
+ ```json
2031
+ {
2032
+ "pagination": {
2033
+ "totalResults": 1,
2034
+ "returnedResults": 1,
2035
+ "perPage": 20,
2036
+ "totalPages": 1,
2037
+ "nextCursor": null,
2038
+ "nextPage": null
2039
+ },
2040
+ "data": [
2041
+ {
2042
+ "id": "clg7n5p3q1r9s7t5u3v1w9y7",
2043
+ "name": "Onboarding",
2044
+ "description": "Signup and activation flows",
2045
+ "createdAt": "2025-06-29T07:47:39.370Z",
2046
+ "updatedAt": "2025-06-29T07:47:39.370Z"
2047
+ }
2048
+ ]
2049
+ }
2050
+ ```
2051
+
2052
+ ---
2053
+
2054
+ ### WorkflowGroups.create()
2055
+
2056
+ Create a workflow group.
2057
+
2058
+ [API Reference](https://loops.so/docs/api-reference/create-workflow-group)
2059
+
2060
+ #### Parameters
2061
+
2062
+ | Name | Type | Required | Notes |
2063
+ | ------------- | ------ | -------- | --------------------------------------- |
2064
+ | `name` | string | Yes | Cannot be the reserved name "Unsorted". |
2065
+ | `description` | string | No | An optional description for the group. |
2066
+
2067
+ #### Example
2068
+
2069
+ ```ruby
2070
+ response = LoopsSdk::WorkflowGroups.create(name: "Onboarding", description: "Signup flows")
2071
+ ```
2072
+
2073
+ #### Response
2074
+
2075
+ ```json
2076
+ {
2077
+ "id": "clg7n5p3q1r9s7t5u3v1w9y7",
2078
+ "name": "Onboarding",
2079
+ "description": "Signup flows",
2080
+ "createdAt": "2025-06-29T07:47:39.370Z",
2081
+ "updatedAt": "2025-06-29T07:47:39.370Z"
2082
+ }
2083
+ ```
2084
+
2085
+ ---
2086
+
2087
+ ### WorkflowGroups.get()
2088
+
2089
+ Get a workflow group by ID.
2090
+
2091
+ [API Reference](https://loops.so/docs/api-reference/get-workflow-group)
2092
+
2093
+ #### Parameters
2094
+
2095
+ | Name | Type | Required | Notes |
2096
+ | ------------------- | ------ | -------- | ----- |
2097
+ | `workflow_group_id` | string | Yes | |
2098
+
2099
+ #### Example
2100
+
2101
+ ```ruby
2102
+ response = LoopsSdk::WorkflowGroups.get(workflow_group_id: "clw8x0y2z0300ycc1lrstvw78")
2103
+ ```
2104
+
2105
+ #### Response
2106
+
2107
+ ```json
2108
+ {
2109
+ "id": "clg7n5p3q1r9s7t5u3v1w9y7",
2110
+ "name": "Onboarding",
2111
+ "description": "Signup and activation flows",
2112
+ "createdAt": "2025-06-29T07:47:39.370Z",
2113
+ "updatedAt": "2025-06-29T07:47:39.370Z"
2114
+ }
2115
+ ```
2116
+
2117
+ ---
2118
+
2119
+ ### WorkflowGroups.update()
2120
+
2121
+ Update a workflow group's name or description.
2122
+
2123
+ [API Reference](https://loops.so/docs/api-reference/update-workflow-group)
2124
+
2125
+ #### Parameters
2126
+
2127
+ | Name | Type | Required | Notes |
2128
+ | ------------------- | ------ | -------- | --------------------------------------- |
2129
+ | `workflow_group_id` | string | Yes | |
2130
+ | `name` | string | No | Cannot be the reserved name "Unsorted". |
2131
+ | `description` | string | No | |
2132
+
2133
+ At least one field must be provided.
2134
+
2135
+ #### Example
2136
+
2137
+ ```ruby
2138
+ response = LoopsSdk::WorkflowGroups.update(
2139
+ workflow_group_id: "clw8x0y2z0300ycc1lrstvw78",
2140
+ name: "Updated name"
2141
+ )
2142
+ ```
2143
+
2144
+ #### Response
2145
+
2146
+ ```json
2147
+ {
2148
+ "id": "clg7n5p3q1r9s7t5u3v1w9y7",
2149
+ "name": "Updated name",
2150
+ "description": "Signup and activation flows",
2151
+ "createdAt": "2025-06-29T07:47:39.370Z",
2152
+ "updatedAt": "2025-06-29T07:47:39.370Z"
2153
+ }
2154
+ ```
2155
+
2156
+ ---
2157
+
1934
2158
  ### AudienceSegments.list()
1935
2159
 
1936
2160
  List audience segments.
@@ -2102,7 +2326,9 @@ response = LoopsSdk::Workflows.list
2102
2326
  "data": [
2103
2327
  {
2104
2328
  "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2329
+ "url": "https://app.loops.so/workflows/clw1a3b5c7d9e1f3g5h7i9j1",
2105
2330
  "name": "Onboarding",
2331
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2106
2332
  "createdAt": "2025-06-29T07:47:39.370Z",
2107
2333
  "updatedAt": "2025-06-29T07:47:39.370Z"
2108
2334
  }
@@ -2120,11 +2346,12 @@ Create a draft workflow with a blank trigger and exit node.
2120
2346
 
2121
2347
  #### Parameters
2122
2348
 
2123
- | Name | Type | Required | Notes |
2124
- | ----------------- | ------ | -------- | ---------------------------------------------- |
2125
- | `name` | string | Yes | |
2126
- | `description` | string | No | |
2127
- | `mailing_list_id` | string | No | The mailing list the workflow sends to. |
2349
+ | Name | Type | Required | Notes |
2350
+ | -------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------- |
2351
+ | `name` | string | Yes | |
2352
+ | `description` | string | No | |
2353
+ | `mailing_list_id` | string | No | The mailing list the workflow sends to. |
2354
+ | `workflow_group_id` | string | No | The workflow group to create this workflow in. Defaults to the team's Unsorted group when omitted. |
2128
2355
 
2129
2356
  #### Example
2130
2357
 
@@ -2139,6 +2366,7 @@ response = LoopsSdk::Workflows.create(name: "Welcome series")
2139
2366
  "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2140
2367
  "status": "Draft",
2141
2368
  "name": "Welcome series",
2369
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2142
2370
  "mailingListId": null,
2143
2371
  "rootNodeId": "cf16k73gq014h3mmj5b6jdi9r",
2144
2372
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
@@ -2182,6 +2410,7 @@ response = LoopsSdk::Workflows.get(workflow_id: "cls9t2u4v0210rx20jpuary23")
2182
2410
  "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2183
2411
  "status": "Draft",
2184
2412
  "name": "Onboarding",
2413
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2185
2414
  "mailingListId": null,
2186
2415
  "rootNodeId": "cf16k73gq014h3mmj5b6jdi9r",
2187
2416
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
@@ -2214,8 +2443,9 @@ Update a workflow's display properties. To change the mailing list, use `Workflo
2214
2443
  | `expected_revision_id` | string | Yes | The `workflowRevisionId` from the latest read or mutation. Pass `nil` for older workflows. |
2215
2444
  | `name` | string | No | |
2216
2445
  | `description` | string | No | |
2446
+ | `workflow_group_id` | string | No | The workflow group to move this workflow into. |
2217
2447
 
2218
- At least one of `name` or `description` must be provided.
2448
+ At least one of `name`, `description`, or `workflow_group_id` must be provided.
2219
2449
 
2220
2450
  #### Example
2221
2451
 
@@ -2234,6 +2464,7 @@ response = LoopsSdk::Workflows.update(
2234
2464
  "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2235
2465
  "status": "Draft",
2236
2466
  "name": "Updated name",
2467
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2237
2468
  "mailingListId": null,
2238
2469
  "rootNodeId": "cf16k73gq014h3mmj5b6jdi9r",
2239
2470
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
@@ -2252,6 +2483,42 @@ response = LoopsSdk::Workflows.update(
2252
2483
 
2253
2484
  ---
2254
2485
 
2486
+ ### Workflows.delete()
2487
+
2488
+ Delete a workflow. Successful deletion returns no content (`nil`). If the workflow is currently sending or has queued contacts, the API returns `409 Conflict` — retry with `confirm_delete: true` to delete the workflow, stop sending, and cancel queued contacts.
2489
+
2490
+ [API Reference](https://loops.so/docs/api-reference/delete-workflow)
2491
+
2492
+ #### Parameters
2493
+
2494
+ | Name | Type | Required | Notes |
2495
+ | ---------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------- |
2496
+ | `workflow_id` | string | Yes | |
2497
+ | `expected_revision_id` | string | Yes | The `workflowRevisionId` from the latest read or mutation. Pass `nil` for older workflows. |
2498
+ | `confirm_delete` | boolean | No | Set to `true` after a confirmation-required `409` to confirm deleting a sending workflow or a workflow with queued contacts. |
2499
+
2500
+ #### Example
2501
+
2502
+ ```ruby
2503
+ LoopsSdk::Workflows.delete(
2504
+ workflow_id: "cls9t2u4v0210rx20jpuary23",
2505
+ expected_revision_id: "rev_123"
2506
+ )
2507
+
2508
+ # After a 409 confirming a sending or queued workflow:
2509
+ LoopsSdk::Workflows.delete(
2510
+ workflow_id: "cls9t2u4v0210rx20jpuary23",
2511
+ expected_revision_id: "rev_123",
2512
+ confirm_delete: true
2513
+ )
2514
+ ```
2515
+
2516
+ #### Response
2517
+
2518
+ `nil` (HTTP `204 No Content`)
2519
+
2520
+ ---
2521
+
2255
2522
  ### Workflows.change_mailing_list()
2256
2523
 
2257
2524
  Dry run or apply a workflow mailing list change. If queued contacts would be removed, the API returns `"status": "queuedContactsFound"` — retry with `queued_contact_policy: "discard"` to apply.
@@ -2287,7 +2554,25 @@ response = LoopsSdk::Workflows.change_mailing_list(
2287
2554
  "mailingListId": "cm06f5v0e45nf0ml5754o9cix",
2288
2555
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
2289
2556
  "queuedContactCount": 0,
2290
- "queuedContactLimitReached": false
2557
+ "workflow": {
2558
+ "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2559
+ "status": "Draft",
2560
+ "name": "Welcome series",
2561
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2562
+ "mailingListId": "cm06f5v0e45nf0ml5754o9cix",
2563
+ "rootNodeId": "cln8p0q2r4s6t8u0v2w4x6z8",
2564
+ "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
2565
+ "nodes": {
2566
+ "cln8p0q2r4s6t8u0v2w4x6z8": {
2567
+ "typeName": "BlankTrigger",
2568
+ "nextNodeIds": ["cln1a3b5c7d9e1f3g5h7i9j1"]
2569
+ },
2570
+ "cln1a3b5c7d9e1f3g5h7i9j1": {
2571
+ "typeName": "ExitAction",
2572
+ "nextNodeIds": []
2573
+ }
2574
+ }
2575
+ }
2291
2576
  }
2292
2577
  ```
2293
2578
 
@@ -2328,9 +2613,52 @@ response = LoopsSdk::Workflows.get_node(workflow_id: "cls9t2u4v0210rx20jpuary23"
2328
2613
 
2329
2614
  ---
2330
2615
 
2616
+ ### Workflows.get_node_metrics()
2617
+
2618
+ Get all-time engagement metrics for a workflow email node.
2619
+
2620
+ [API Reference](https://loops.so/docs/api-reference/get-workflow-node-metrics)
2621
+
2622
+ #### Parameters
2623
+
2624
+ | Name | Type | Required | Notes |
2625
+ | ------------- | ------ | -------- | ----- |
2626
+ | `workflow_id` | string | Yes | |
2627
+ | `node_id` | string | Yes | |
2628
+
2629
+ #### Example
2630
+
2631
+ ```ruby
2632
+ response = LoopsSdk::Workflows.get_node_metrics(
2633
+ workflow_id: "cls9t2u4v0210rx20jpuary23",
2634
+ node_id: "clt0u3v5w0232sy31kqvbzs34"
2635
+ )
2636
+ ```
2637
+
2638
+ #### Response
2639
+
2640
+ ```json
2641
+ {
2642
+ "sends": 4210,
2643
+ "opens": 1922,
2644
+ "clicks": 301,
2645
+ "unsubscribes": 12,
2646
+ "spamReports": 1,
2647
+ "hardBounces": 9,
2648
+ "softBounces": 13
2649
+ }
2650
+ ```
2651
+
2652
+ ---
2653
+
2331
2654
  ### Workflows.create_node()
2332
2655
 
2333
- Create a new default workflow node. Use `insert_mode: "between"` with `from_node_id`/`to_node_id`, or `insert_mode: "before"` with `before_node_id`.
2656
+ Create a new default workflow node.
2657
+
2658
+ Use `insert_mode` to choose placement:
2659
+ - `between` — insert between `from_node_id` and `to_node_id`
2660
+ - `before` — insert before `to_node_id`
2661
+ - `after` — insert after `from_node_id` (source must have exactly one outgoing connection)
2334
2662
 
2335
2663
  [API Reference](https://loops.so/docs/api-reference/create-workflow-node)
2336
2664
 
@@ -2340,11 +2668,11 @@ Create a new default workflow node. Use `insert_mode: "between"` with `from_node
2340
2668
  | ---------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------- |
2341
2669
  | `workflow_id` | string | Yes | |
2342
2670
  | `expected_revision_id` | string | Yes | Pass `nil` for older workflows. |
2343
- | `insert_mode` | string | Yes | `between` or `before`. |
2671
+ | `insert_mode` | string | Yes | `between`, `before`, or `after`. |
2344
2672
  | `node_type_name` | string | Yes | One of `AudienceFilter`, `BranchNode`, `ExperimentBranchNode`, `TimerAction`, `SendEmailAction`, `VariantNode`. |
2345
- | `from_node_id` | string | No | Required when `insert_mode` is `between`. |
2346
- | `to_node_id` | string | No | Required when `insert_mode` is `between`. |
2347
- | `before_node_id` | string | No | Required when `insert_mode` is `before`. |
2673
+ | `from_node_id` | string | Cond. | Required for `between` and `after`. Not permitted for `before`. |
2674
+ | `to_node_id` | string | Cond. | Required for `between`. For `before`, provide this or `before_node_id` (not both). Not permitted for `after`. |
2675
+ | `before_node_id` | string | Cond. | Deprecated. For `before`, provide this or `to_node_id` (not both). |
2348
2676
 
2349
2677
  #### Example
2350
2678
 
@@ -2357,6 +2685,22 @@ response = LoopsSdk::Workflows.create_node(
2357
2685
  from_node_id: "node_a",
2358
2686
  to_node_id: "node_b"
2359
2687
  )
2688
+
2689
+ response = LoopsSdk::Workflows.create_node(
2690
+ workflow_id: "cls9t2u4v0210rx20jpuary23",
2691
+ expected_revision_id: "rev_123",
2692
+ insert_mode: "after",
2693
+ node_type_name: "TimerAction",
2694
+ from_node_id: "node_a"
2695
+ )
2696
+
2697
+ response = LoopsSdk::Workflows.create_node(
2698
+ workflow_id: "cls9t2u4v0210rx20jpuary23",
2699
+ expected_revision_id: "rev_123",
2700
+ insert_mode: "before",
2701
+ node_type_name: "TimerAction",
2702
+ to_node_id: "node_b"
2703
+ )
2360
2704
  ```
2361
2705
 
2362
2706
  #### Response
@@ -2375,6 +2719,7 @@ response = LoopsSdk::Workflows.create_node(
2375
2719
  "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2376
2720
  "status": "Draft",
2377
2721
  "name": "Welcome series",
2722
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2378
2723
  "mailingListId": null,
2379
2724
  "rootNodeId": "node_a",
2380
2725
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
@@ -2436,7 +2781,28 @@ response = LoopsSdk::Workflows.update_node(
2436
2781
  "nextNodeIds": ["cf16k73gq014h3mmj5b4jdifg"],
2437
2782
  "amount": 2,
2438
2783
  "unit": "d",
2439
- "workflowRevisionId": "clrev0w0r1k2f3l4o5w6"
2784
+ "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
2785
+ "workflow": {
2786
+ "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2787
+ "status": "Draft",
2788
+ "name": "Welcome series",
2789
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2790
+ "mailingListId": null,
2791
+ "rootNodeId": "cln8p0q2r4s6t8u0v2w4x6z8",
2792
+ "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
2793
+ "nodes": {
2794
+ "cln8p0q2r4s6t8u0v2w4x6z8": {
2795
+ "typeName": "TimerAction",
2796
+ "nextNodeIds": ["cf16k73gq014h3mmj5b4jdifg"],
2797
+ "amount": 2,
2798
+ "unit": "d"
2799
+ },
2800
+ "cf16k73gq014h3mmj5b4jdifg": {
2801
+ "typeName": "ExitAction",
2802
+ "nextNodeIds": []
2803
+ }
2804
+ }
2805
+ }
2440
2806
  }
2441
2807
  ```
2442
2808
 
@@ -2477,7 +2843,25 @@ response = LoopsSdk::Workflows.delete_node(
2477
2843
  "nodeIds": ["clt0u3v5w0232sy31kqvbzs34"],
2478
2844
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
2479
2845
  "queuedContactCount": 0,
2480
- "queuedContactLimitReached": false
2846
+ "workflow": {
2847
+ "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2848
+ "status": "Draft",
2849
+ "name": "Welcome series",
2850
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2851
+ "mailingListId": null,
2852
+ "rootNodeId": "cln8p0q2r4s6t8u0v2w4x6z8",
2853
+ "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
2854
+ "nodes": {
2855
+ "cln8p0q2r4s6t8u0v2w4x6z8": {
2856
+ "typeName": "BlankTrigger",
2857
+ "nextNodeIds": ["cln1a3b5c7d9e1f3g5h7i9j1"]
2858
+ },
2859
+ "cln1a3b5c7d9e1f3g5h7i9j1": {
2860
+ "typeName": "ExitAction",
2861
+ "nextNodeIds": []
2862
+ }
2863
+ }
2864
+ }
2481
2865
  }
2482
2866
  ```
2483
2867
 
@@ -2522,6 +2906,7 @@ response = LoopsSdk::Workflows.add_branch(
2522
2906
  "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2523
2907
  "status": "Draft",
2524
2908
  "name": "Welcome series",
2909
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2525
2910
  "mailingListId": null,
2526
2911
  "rootNodeId": "clt0u3v5w0232sy31kqvbzs34",
2527
2912
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
@@ -2541,6 +2926,66 @@ response = LoopsSdk::Workflows.add_branch(
2541
2926
 
2542
2927
  ---
2543
2928
 
2929
+ ### Workflows.reroute_node()
2930
+
2931
+ Reroute a source node's outgoing connection to another valid target node. The source node must have exactly one outgoing connection. Branch and experiment branch nodes cannot be rerouted with this endpoint.
2932
+
2933
+ [API Reference](https://loops.so/docs/api-reference/reroute-node-connection)
2934
+
2935
+ #### Parameters
2936
+
2937
+ | Name | Type | Required | Notes |
2938
+ | ---------------------- | ------ | -------- | ------------------------------------------ |
2939
+ | `workflow_id` | string | Yes | |
2940
+ | `node_id` | string | Yes | The source node whose connection to move. |
2941
+ | `expected_revision_id` | string | Yes | Pass `nil` for older workflows. |
2942
+ | `new_target_node_id` | string | Yes | The node that should receive the connection. |
2943
+
2944
+ #### Example
2945
+
2946
+ ```ruby
2947
+ response = LoopsSdk::Workflows.reroute_node(
2948
+ workflow_id: "cls9t2u4v0210rx20jpuary23",
2949
+ node_id: "clt0u3v5w0232sy31kqvbzs34",
2950
+ expected_revision_id: "rev_123",
2951
+ new_target_node_id: "cln3c5d7e9f1g3h5i7j9k1l3"
2952
+ )
2953
+ ```
2954
+
2955
+ #### Response
2956
+
2957
+ ```json
2958
+ {
2959
+ "id": "cln8p0q2r4s6t8u0v2w4x6z8",
2960
+ "typeName": "SignupTrigger",
2961
+ "nextNodeIds": ["cln3c5d7e9f1g3h5i7j9k1l3"],
2962
+ "workflowRevisionId": "clx7a3b5c7d9e1f3g5h7i9j2",
2963
+ "workflow": {
2964
+ "id": "clw1a3b5c7d9e1f3g5h7i9j1",
2965
+ "workflowRevisionId": "clx7a3b5c7d9e1f3g5h7i9j2",
2966
+ "status": "Draft",
2967
+ "name": "Welcome series",
2968
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
2969
+ "mailingListId": "clm2k8j4h6g0f8d6s4a2b0z8",
2970
+ "rootNodeId": "cln8p0q2r4s6t8u0v2w4x6z8",
2971
+ "nodes": {
2972
+ "cln8p0q2r4s6t8u0v2w4x6z8": {
2973
+ "typeName": "SignupTrigger",
2974
+ "nextNodeIds": ["cln3c5d7e9f1g3h5i7j9k1l3"]
2975
+ },
2976
+ "cln3c5d7e9f1g3h5i7j9k1l3": {
2977
+ "typeName": "SendEmailAction",
2978
+ "nextNodeIds": [],
2979
+ "emailMessageId": "cle5f7g9h1i3j5k7l9m1n3p5",
2980
+ "subject": "Welcome to Acme!"
2981
+ }
2982
+ }
2983
+ }
2984
+ }
2985
+ ```
2986
+
2987
+ ---
2988
+
2544
2989
  ### Workflows.delete_node_recursive()
2545
2990
 
2546
2991
  Delete a node and its downstream subtree.
@@ -2579,7 +3024,25 @@ response = LoopsSdk::Workflows.delete_node_recursive(
2579
3024
  ],
2580
3025
  "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
2581
3026
  "queuedContactCount": 0,
2582
- "queuedContactLimitReached": false
3027
+ "workflow": {
3028
+ "id": "clw1a3b5c7d9e1f3g5h7i9j1",
3029
+ "status": "Draft",
3030
+ "name": "Welcome series",
3031
+ "workflowGroupId": "clg7n5p3q1r9s7t5u3v1w9y7",
3032
+ "mailingListId": null,
3033
+ "rootNodeId": "cln8p0q2r4s6t8u0v2w4x6z8",
3034
+ "workflowRevisionId": "clrev0w0r1k2f3l4o5w6",
3035
+ "nodes": {
3036
+ "cln8p0q2r4s6t8u0v2w4x6z8": {
3037
+ "typeName": "BlankTrigger",
3038
+ "nextNodeIds": ["cln1a3b5c7d9e1f3g5h7i9j1"]
3039
+ },
3040
+ "cln1a3b5c7d9e1f3g5h7i9j1": {
3041
+ "typeName": "ExitAction",
3042
+ "nextNodeIds": []
3043
+ }
3044
+ }
3045
+ }
2583
3046
  }
2584
3047
  ```
2585
3048
 
@@ -9,6 +9,8 @@ module LoopsSdk
9
9
  case response.status
10
10
  when 200, 201
11
11
  JSON.parse(response.body)
12
+ when 204
13
+ nil
12
14
  when 429
13
15
  limit = response.headers["x-ratelimit-limit"]
14
16
  remaining = response.headers["x-ratelimit-remaining"]
@@ -34,6 +34,10 @@ module LoopsSdk
34
34
  }.compact
35
35
  make_request(method: :post, path: "v1/campaigns/#{campaign_id}", body: body)
36
36
  end
37
+
38
+ def metrics(campaign_id:)
39
+ make_request(method: :get, path: "v1/campaigns/#{campaign_id}/metrics")
40
+ end
37
41
  end
38
42
  end
39
43
  end
@@ -29,6 +29,10 @@ module LoopsSdk
29
29
  make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}/publish")
30
30
  end
31
31
 
32
+ def metrics(transactional_id:)
33
+ make_request(method: :get, path: "v1/transactional-emails/#{transactional_id}/metrics")
34
+ end
35
+
32
36
  def send(transactional_id:, email:, add_to_audience: false, data_variables: {}, attachments: [], headers: {})
33
37
  attachments = attachments.map do |attachment|
34
38
  attachment.transform_keys { |key| key == :content_type ? :contentType : key }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LoopsSdk
4
- VERSION = "2.4.0"
4
+ VERSION = "2.6.0"
5
5
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoopsSdk
4
+ class WorkflowGroups < Base
5
+ class << self
6
+ def list(perPage: 20, cursor: nil)
7
+ make_request(method: :get, path: "v1/workflow-groups", params: { perPage: perPage, cursor: cursor })
8
+ end
9
+
10
+ def create(name:, description: nil)
11
+ body = { name: name, description: description }.compact
12
+ make_request(method: :post, path: "v1/workflow-groups", body: body)
13
+ end
14
+
15
+ def get(workflow_group_id:)
16
+ make_request(method: :get, path: "v1/workflow-groups/#{workflow_group_id}")
17
+ end
18
+
19
+ def update(workflow_group_id:, name: nil, description: nil)
20
+ body = { name: name, description: description }.compact
21
+ make_request(method: :post, path: "v1/workflow-groups/#{workflow_group_id}", body: body)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -7,11 +7,12 @@ module LoopsSdk
7
7
  make_request(method: :get, path: "v1/workflows", params: { perPage: perPage, cursor: cursor })
8
8
  end
9
9
 
10
- def create(name:, description: nil, mailing_list_id: nil)
10
+ def create(name:, description: nil, mailing_list_id: nil, workflow_group_id: nil)
11
11
  body = {
12
12
  name: name,
13
13
  description: description,
14
- mailingListId: mailing_list_id
14
+ mailingListId: mailing_list_id,
15
+ workflowGroupId: workflow_group_id
15
16
  }.compact
16
17
  make_request(method: :post, path: "v1/workflows", body: body)
17
18
  end
@@ -20,17 +21,28 @@ module LoopsSdk
20
21
  make_request(method: :get, path: "v1/workflows/#{workflow_id}")
21
22
  end
22
23
 
23
- def update(workflow_id:, expected_revision_id:, name: nil, description: nil)
24
+ def update(workflow_id:, expected_revision_id:, name: nil, description: nil, workflow_group_id: nil)
24
25
  body = {
25
26
  expectedRevisionId: expected_revision_id,
26
27
  name: name,
27
- description: description
28
+ description: description,
29
+ workflowGroupId: workflow_group_id
28
30
  }.compact
29
31
  # expectedRevisionId may be null for older workflows; always include it
30
32
  body[:expectedRevisionId] = expected_revision_id
31
33
  make_request(method: :post, path: "v1/workflows/#{workflow_id}", body: body)
32
34
  end
33
35
 
36
+ def delete(workflow_id:, expected_revision_id:, confirm_delete: nil)
37
+ body = {
38
+ expectedRevisionId: expected_revision_id,
39
+ confirmDelete: confirm_delete
40
+ }.compact
41
+ # expectedRevisionId may be null for older workflows; always include it
42
+ body[:expectedRevisionId] = expected_revision_id
43
+ make_request(method: :delete, path: "v1/workflows/#{workflow_id}", body: body)
44
+ end
45
+
34
46
  def change_mailing_list(workflow_id:, expected_revision_id:, mailing_list_id:, dry_run: nil, queued_contact_policy: nil)
35
47
  body = {
36
48
  expectedRevisionId: expected_revision_id,
@@ -48,6 +60,10 @@ module LoopsSdk
48
60
  make_request(method: :get, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}")
49
61
  end
50
62
 
63
+ def get_node_metrics(workflow_id:, node_id:)
64
+ make_request(method: :get, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}/metrics")
65
+ end
66
+
51
67
  def create_node(
52
68
  workflow_id:,
53
69
  expected_revision_id:,
@@ -60,12 +76,36 @@ module LoopsSdk
60
76
  body = {
61
77
  expectedRevisionId: expected_revision_id,
62
78
  insertMode: insert_mode,
63
- nodeTypeName: node_type_name,
64
- fromNodeId: from_node_id,
65
- toNodeId: to_node_id,
66
- beforeNodeId: before_node_id
67
- }.compact
68
- body[:expectedRevisionId] = expected_revision_id
79
+ nodeTypeName: node_type_name
80
+ }
81
+
82
+ case insert_mode
83
+ when "between"
84
+ raise ArgumentError, "from_node_id and to_node_id are required when insert_mode is \"between\"." if from_node_id.nil? || to_node_id.nil?
85
+ raise ArgumentError, "before_node_id is not permitted when insert_mode is \"between\"." unless before_node_id.nil?
86
+
87
+ body[:fromNodeId] = from_node_id
88
+ body[:toNodeId] = to_node_id
89
+ when "before"
90
+ raise ArgumentError, "from_node_id is not permitted when insert_mode is \"before\"." unless from_node_id.nil?
91
+ raise ArgumentError, "Provide either to_node_id or before_node_id when insert_mode is \"before\", not both." if !to_node_id.nil? && !before_node_id.nil?
92
+ raise ArgumentError, "to_node_id or before_node_id is required when insert_mode is \"before\"." if to_node_id.nil? && before_node_id.nil?
93
+
94
+ if !to_node_id.nil?
95
+ body[:toNodeId] = to_node_id
96
+ else
97
+ body[:beforeNodeId] = before_node_id
98
+ end
99
+ when "after"
100
+ raise ArgumentError, "from_node_id is required when insert_mode is \"after\"." if from_node_id.nil?
101
+ raise ArgumentError, "to_node_id is not permitted when insert_mode is \"after\"." unless to_node_id.nil?
102
+ raise ArgumentError, "before_node_id is not permitted when insert_mode is \"after\"." unless before_node_id.nil?
103
+
104
+ body[:fromNodeId] = from_node_id
105
+ else
106
+ raise ArgumentError, "insert_mode must be \"between\", \"before\", or \"after\"."
107
+ end
108
+
69
109
  make_request(method: :post, path: "v1/workflows/#{workflow_id}/nodes", body: body)
70
110
  end
71
111
 
@@ -92,6 +132,14 @@ module LoopsSdk
92
132
  make_request(method: :post, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}/add-branch", body: body)
93
133
  end
94
134
 
135
+ def reroute_node(workflow_id:, node_id:, expected_revision_id:, new_target_node_id:)
136
+ body = {
137
+ expectedRevisionId: expected_revision_id,
138
+ newTargetNodeId: new_target_node_id
139
+ }
140
+ make_request(method: :post, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}/reroute", body: body)
141
+ end
142
+
95
143
  def delete_node_recursive(workflow_id:, node_id:, expected_revision_id:, dry_run: nil, queued_contact_policy: nil)
96
144
  body = {
97
145
  expectedRevisionId: expected_revision_id,
data/lib/loops_sdk.rb CHANGED
@@ -20,6 +20,7 @@ require_relative "loops_sdk/email_messages"
20
20
  require_relative "loops_sdk/uploads"
21
21
  require_relative "loops_sdk/audience_segments"
22
22
  require_relative "loops_sdk/workflows"
23
+ require_relative "loops_sdk/workflow_groups"
23
24
  require_relative "loops_sdk/event_patterns"
24
25
  require_relative "loops_sdk/transactional_groups"
25
26
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loops_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Rowden
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-04 00:00:00.000000000 Z
11
+ date: 2026-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -53,6 +53,7 @@ files:
53
53
  - lib/loops_sdk/transactional_groups.rb
54
54
  - lib/loops_sdk/uploads.rb
55
55
  - lib/loops_sdk/version.rb
56
+ - lib/loops_sdk/workflow_groups.rb
56
57
  - lib/loops_sdk/workflows.rb
57
58
  homepage: https://loops.so/docs/api-reference
58
59
  licenses: