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 of elements. For example, sending a notification to a group of users.

List of users

{
"data": {
"email_list": [
"jane.doe@example.com",
"john_smith88@gmail.com",
"alice.w@yahoo.com",
"support@mybusiness.net",
"data-report-user@company.org",
"tester_01@outlook.com"
]
}
}

Usage Compound Action

steps:
- action:
action_name: mw.batch_get_users_by_email
output_key: list_of_users
input_args:
user_emails: data.email_list
- for:
output_key: list_of_notifications
each: record
in: data.list_of_users.user_records
index: idx
steps:
- notify:
output_key: notification_output
recipient_id: record.lookup_id
message:
RENDER():
args:
user_name: record.user.full_name
template: Hi {{user_name}}, you have not submitted your project update this week. Please do before the end of the day!
- action:
action_name: do_some_action_for_user
output_key: action_result
input_args:
user: record.user
...

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

{
"data": {
"duplicated_fruit_list": ["apple", "banana", "cherry", "apple", "banana", "date"]
}
}
seen = set()
unique = []
for x in elements:
if x in seen:
continue
seen.add(x)
unique.append(x)
return unique

Usage in Compound Action

steps:
- action:
action_name: deduplicate_list
output_key: deduplicate_list_result
input_args:
elements: data.duplicated_fruit_list
- return:
output_mapper:
unique_list: data.deduplicate_list_result

Result

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

Deduplicating a list of objects

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

seen = set()
out = []
for obj in elements:
if not isinstance(obj, dict) or key_field not in obj:
continue
k = obj[key_field]
if k in seen:
continue
seen.add(k)
out.append(obj)
return out

Usage in Compound Action

steps:
- action:
action_name: deduplicate_object_list
output_key: deduplicate_list_result
input_args:
elements: data.input_list
key_field: '''email'''
- return:
output_mapper:
unique_list: data.deduplicate_list_result

Sort Alphabetically

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

Input Arguments

{
"data":{
"fruits": ["red", "green", "blue", "yellow", "purple"]
}
}

Compound Action

steps:
- action:
action_name: enter_fruits_into_system
output_key: fruits_result
input_args:
sorted_fruits:
SORT():
items: data.fruits
key: item

Result

{
"sorted_fruits": [
"blue",
"green",
"purple",
"red",
"yellow"
]
}

Sort Timestamps

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

Sorting a list of timestamps

input

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

Compound Action

steps:
- action:
action_name: enter_timestamps
output_key: timestamps_result
input_args:
sorted_timestamps:
SORT():
items: data.ts
key: item.$PARSE_TIME()

Result

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

Sorting a list of objects by timestamp

Input

{
"data": {
"finances": [
{
"id": "txn_k5p1",
"ts": "2025-09-20T10:30:00-06:00",
"amount": 2500.0
},
{
"id": "txn_z7h3",
"ts": "2025-09-22T08:15:00-06:00",
"amount": -29.99
},
{
"id": "txn_8x4f",
"ts": "2025-09-19T17:14:04-06:00",
"amount": -8.5
},
{
"id": "txn_a2d9",
"ts": "2025-09-19T21:00:00-06:00",
"amount": 150.75
}
]
}
}

Compound Action

steps:
- action:
action_name: enter_finances
output_key: finances_result
input_args:
sorted_financials:
SORT():
items: data.finances
key: item.ts.$PARSE_TIME()

Result

{
"sorted_financials": [
{
"amount": -8.5,
"id": "txn_8x4f",
"ts": "2025-09-19T17:14:04-06:00"
},
{
"amount": 150.75,
"id": "txn_a2d9",
"ts": "2025-09-19T21:00:00-06:00"
},
{
"amount": 2500,
"id": "txn_k5p1",
"ts": "2025-09-20T10:30:00-06:00"
},
{
"amount": -29.99,
"id": "txn_z7h3",
"ts": "2025-09-22T08:15:00-06:00"
}
]
}

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:

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

Compound Action

steps:
# Step 1: Generate a range to iterate over (e.g., max 10 pages)
- action:
action_name: generate_page_range
output_key: page_range
input_args:
max_pages: 10
# Step 2: Paginate through the API
- for:
each: page
index: page_index
in: data.page_range
output_key: paginated_results
steps:
- action:
action_name: fetch_items_page
output_key: fetch_result
condition: page_index == 0 OR data.paginated_results[page_index - 1].fetch_result.nextPageToken != nil
input_args:
page_size: 100
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:

# Action: flatten_paginated_results
# Input: paginated_results (list of dicts from the for loop)
all_items = []
for page in paginated_results:
result = page.get("fetch_result")
if result and result.get("items"):
all_items.extend(result["items"])
return 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):

steps:
- action:
action_name: generate_page_range
output_key: poll_range
input_args:
max_pages: 12
- action:
action_name: send_okta_push_verification
output_key: push_result
input_args:
user_id: data.user_id
- for:
each: attempt
index: attempt_index
in: data.poll_range
output_key: poll_results
steps:
- action:
action_name: check_okta_push_status
output_key: status_check
condition: >
attempt_index == 0
OR data.poll_results[attempt_index - 1].status_check.factorResult == 'WAITING'
input_args:
poll_url: data.push_result.poll_url
- delay:
seconds: 5
condition: >
data.poll_results[attempt_index].status_check.factorResult == 'WAITING'

Easiest way to multiline string usage in YAML

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

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

Compound Action

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