Compound Action Patterns

View as Markdown

This document’s purpose is to accelerate your development process by providing a collection of reusable patterns for common tasks.

Instead of showcasing entire** end-to-end** use cases, this guide focuses on individual, common “steps” you’ll encounter while building. For each pattern, we recommend the most efficient method, whether it’s an LLM action, a Moveworks Data Mapper expression, a DSL query, or a Python Script.

Let’s dive into the patterns.

Performing Actions in Batch

Problem: You want to perform a series of actions repetitively for a list elements. For example, sending a notification to a group of users.

List of users

1{
2 "data": {
3 "email_list": [
4 "jane.doe@example.com",
5 "john_smith88@gmail.com",
6 "alice.w@yahoo.com",
7 "support@mybusiness.net",
8 "data-report-user@company.org",
9 "tester_01@outlook.com"
10 ]
11 }
12}

Usage Compound Action

1steps:
2 - action:
3 action_name: mw.batch_get_users_by_email
4 output_key: list_of_users
5 input_args:
6 user_emails: data.email_list
7 - for:
8 output_key: list_of_notifications
9 each: record
10 in: data.list_of_users.user_records
11 index: idx
12 steps:
13 - notify:
14 output_key: notification_output
15 recipient_id: record.lookup_id
16 message:
17 RENDER():
18 args:
19 user_name: record.user.full_name
20 template: Hi {{user_name}}, you have not submitted your project update this week. Please do before the end of the day!
21 - action:
22 action_name: do_some_action_for_user
23 output_key: action_result
24 input_args:
25 user: record.user
26...

Deduplicating a List

Problem: You have a list that contains duplicate values, and you need a list with only unique elements.

Deduplicating a list of scalars

Input

1{
2 "data": {
3 "duplicated_fruit_list": ["apple", "banana", "cherry", "apple", "banana", "date"]
4 }
5}
1seen = set()
2unique = []
3for x in elements:
4 if x in seen:
5 continue
6 seen.add(x)
7 unique.append(x)
8return unique

Usage in Compound Action

1steps:
2 - action:
3 action_name: deduplicate_list
4 output_key: deduplicate_list_result
5 input_args:
6 elements: data.duplicated_fruit_list
7 - return:
8 output_mapper:
9 unique_list: data.deduplicate_list_result

Result

1{
2 "unique_list": ["apple", "banana", "cherry", "date"]
3}

Deduplicating a list of objects

In this case we will deduplicating a list of objects that have the same email key

1seen = set()
2out = []
3for obj in elements:
4 if not isinstance(obj, dict) or key_field not in obj:
5 continue
6 k = obj[key_field]
7 if k in seen:
8 continue
9 seen.add(k)
10 out.append(obj)
11return out

Usage in Compound Action

1steps:
2 - action:
3 action_name: deduplicate_object_list
4 output_key: deduplicate_list_result
5 input_args:
6 elements: data.input_list
7 key_field: '''email'''
8 - return:
9 output_mapper:
10 unique_list: data.deduplicate_list_result

Sort Alphabetically

Problem: You need to provide a list of elements sorted alphabetically

Input Arguments

1{
2 "data":{
3 "fruits": ["red", "green", "blue", "yellow", "purple"]
4 }
5}

Compound Action

1steps:
2 - action:
3 action_name: enter_fruits_into_system
4 output_key: fruits_result
5 input_args:
6 sorted_fruits:
7 SORT():
8 items: data.fruits
9 key: item

Result

1{
2 "sorted_fruits": [
3 "blue",
4 "green",
5 "purple",
6 "red",
7 "yellow"
8 ]
9}

Sort Timestamps

Problem: You want to sort a list of based on the timestamp

Sorting a list of timestamps

input

1{
2 "data": {
3 "ts": [
4 "Monday, Sep 22, 2025 at 8:15 AM MDT",
5 "Saturday, Sep 20, 2025 at 10:30 AM MDT",
6 "Friday, Sep 19, 2025 at 5:07 PM MDT",
7 "Friday, Sep 19, 2025 at 9:00 PM MDT"
8 ]
9 }
10}

Compound Action

1steps:
2 - action:
3 action_name: enter_timestamps
4 output_key: timestamps_result
5 input_args:
6 sorted_timestamps:
7 SORT():
8 items: data.ts
9 key: item.$PARSE_TIME()

Result

1{
2 "sorted_timestamps": [
3 "Friday, Sep 19, 2025 at 5:07 PM MDT",
4 "Friday, Sep 19, 2025 at 9:00 PM MDT",
5 "Saturday, Sep 20, 2025 at 10:30 AM MDT",
6 "Monday, Sep 22, 2025 at 8:15 AM MDT"
7 ]
8}

Sorting a list of objects by timestamp

Input

1{
2 "data": {
3 "finances": [
4 {
5 "id": "txn_k5p1",
6 "ts": "2025-09-20T10:30:00-06:00",
7 "amount": 2500.0
8 },
9 {
10 "id": "txn_z7h3",
11 "ts": "2025-09-22T08:15:00-06:00",
12 "amount": -29.99
13 },
14 {
15 "id": "txn_8x4f",
16 "ts": "2025-09-19T17:14:04-06:00",
17 "amount": -8.5
18 },
19 {
20 "id": "txn_a2d9",
21 "ts": "2025-09-19T21:00:00-06:00",
22 "amount": 150.75
23 }
24 ]
25 }
26}

Compound Action

1steps:
2 - action:
3 action_name: enter_finances
4 output_key: finances_result
5 input_args:
6 sorted_financials:
7 SORT():
8 items: data.finances
9 key: item.ts.$PARSE_TIME()

Result

1{
2 "sorted_financials": [
3 {
4 "amount": -8.5,
5 "id": "txn_8x4f",
6 "ts": "2025-09-19T17:14:04-06:00"
7 },
8 {
9 "amount": 150.75,
10 "id": "txn_a2d9",
11 "ts": "2025-09-19T21:00:00-06:00"
12 },
13 {
14 "amount": 2500,
15 "id": "txn_k5p1",
16 "ts": "2025-09-20T10:30:00-06:00"
17 },
18 {
19 "amount": -29.99,
20 "id": "txn_z7h3",
21 "ts": "2025-09-22T08:15:00-06:00"
22 }
23 ]
24}

Pagination with For Loops

Problem: You need to paginate through an API that returns results page-by-page using a pagination token (e.g., nextPageToken), but compound actions don’t support while loops.

Solution: Use a for loop with a fixed range (0 to MAX_PAGES), and on each iteration:

  1. Read the pagination token from the previous iteration’s output using index - 1
  2. Use a condition to skip iterations once there are no more pages
  3. Pass the token (or nil for the first page) into the HTTP action

Do not use parallel expressions with this pattern. Because each iteration reads from the previous iteration’s output in the data tree, the loop must execute sequentially.

Why not a while loop? Open-ended while loops are not supported in compound actions for safety reasons — a misconfigured loop could run indefinitely. The for-loop-with-condition pattern gives you the same pagination behavior with a guaranteed upper bound on iterations.

Setup

First, create a helper action (e.g., a Script Action) that generates a range list for the for loop to iterate over:

1# Action: generate_page_range
2# Input: max_pages (int)
3# Output: list of integers [0, 1, 2, ..., max_pages - 1]
4return list(range(max_pages))

Compound Action

1steps:
2 # Step 1: Generate a range to iterate over (e.g., max 10 pages)
3 - action:
4 action_name: generate_page_range
5 output_key: page_range
6 input_args:
7 max_pages: 10
8
9 # Step 2: Paginate through the API
10 - for:
11 each: page
12 index: page_index
13 in: data.page_range
14 output_key: paginated_results
15 steps:
16 - action:
17 action_name: fetch_items_page
18 output_key: fetch_result
19 condition: page_index == 0 OR data.paginated_results[page_index - 1].fetch_result.nextPageToken != nil
20 input_args:
21 page_size: 100
22 pagination_token: IF (page_index == 0) THEN nil ELSE data.paginated_results[page_index - 1].fetch_result.nextPageToken

How It Works

Iterationpage_indexpagination_tokencondition
First0nil (first page, no token needed)true (always runs)
Subsequent1, 2, ...Read from data.paginated_results[page_index - 1].fetch_result.nextPageTokenOnly runs if previous iteration returned a nextPageToken
After last pageNN/Afalse — skips execution since no nextPageToken was returned

Extracting All Results

After the loop completes, you can use a Script Action to flatten all pages into a single list:

1# Action: flatten_paginated_results
2# Input: paginated_results (list of dicts from the for loop)
3all_items = []
4for page in paginated_results:
5 result = page.get("fetch_result")
6 if result and result.get("items"):
7 all_items.extend(result["items"])
8return all_items

Real-World Example: Polling an API Until Success

This same pattern works for polling use cases (e.g., waiting for an Okta push verification):

1steps:
2 - action:
3 action_name: generate_page_range
4 output_key: poll_range
5 input_args:
6 max_pages: 12
7
8 - action:
9 action_name: send_okta_push_verification
10 output_key: push_result
11 input_args:
12 user_id: data.user_id
13
14 - for:
15 each: attempt
16 index: attempt_index
17 in: data.poll_range
18 output_key: poll_results
19 steps:
20 - action:
21 action_name: check_okta_push_status
22 output_key: status_check
23 condition: >
24 attempt_index == 0
25 OR data.poll_results[attempt_index - 1].status_check.factorResult == 'WAITING'
26 input_args:
27 poll_url: data.push_result.poll_url
28 - delay:
29 seconds: 5
30 condition: >
31 data.poll_results[attempt_index].status_check.factorResult == 'WAITING'

Easiest way to multiline string usage in YAML

Problem: You want to a multiline string’s structure to be maintained

Solution: Use Render() and the | character with your template value indented

Compound Action

1result:
2 display_instruction_for_model:
3 RENDER():
4 template: |
5 Each row = one unique order line item in a customer’s purchase.
6 Uniqueness: LINE_ITEM_ID is the primary key (unique per row).
7 Aggregation grain:
8 - Line item–level -> count rows / COUNT(DISTINCT LINE_ITEM_ID)
9 - Order-level -> COUNT(DISTINCT ORDER_ID)
10 - Customer-level -> COUNT(DISTINCT CUSTOMER_ID)
11 - Product-level -> COUNT(DISTINCT PRODUCT_ID)
12 Rule: Always aggregate at the appropriate grain to ensure metrics align correctly with the business question.