
Imagine you're pulling customer orders from a REST API. The API sends back a massive JSON object with 40 fields per order — customer IDs, internal pricing codes, warehouse flags, timestamps in UTC, raw tax calculations, and a dozen other fields your downstream system doesn't care about. All you need are six clean fields in a specific format, and only the orders that haven't shipped yet. Without the right tools, you'd be writing a separate flow action for every single field, stacking conditions until your flow looks like a plate of spaghetti.
This is exactly the problem that Compose, Select, and Filter Array were built to solve. These three actions are Power Automate's core data transformation toolkit. They let you reshape, remap, and reduce arrays of data before you send them anywhere — to SharePoint, to Excel, to an approval workflow, or back to another API. Together they turn messy, over-stuffed data into exactly the structure your process needs.
By the end of this lesson, you'll be able to take a raw array of records and reshape it into a clean, purposeful dataset using real expressions — not just drag-and-drop guessing. You'll understand how data flows through each action and why it's shaped the way it is.
What you'll learn:
Before we touch any actions, let's get the mental model right. An array is simply a list of items. In Power Automate's world, that list usually contains objects — records with multiple fields each. Think of it like a spreadsheet: the array is the table, and each item in the array is one row.
Here's what a typical array looks like when Power Automate receives it from an API or a data source:
[
{
"order_id": "ORD-1041",
"customer_name": "Priya Sharma",
"status": "pending",
"total_amount": 142.50,
"internal_warehouse_code": "WH-7",
"created_date": "2024-03-15T08:22:00Z"
},
{
"order_id": "ORD-1042",
"customer_name": "Marcus Webb",
"status": "shipped",
"total_amount": 89.00,
"internal_warehouse_code": "WH-3",
"created_date": "2024-03-15T09:45:00Z"
},
{
"order_id": "ORD-1043",
"customer_name": "Leila Okonkwo",
"status": "pending",
"total_amount": 310.00,
"internal_warehouse_code": "WH-7",
"created_date": "2024-03-15T11:00:00Z"
}
]
Three orders, six fields each. The challenge: you only want the pending orders, and your reporting system only needs order_id, customer_name, and total_amount. Everything else is noise. That's exactly what we'll build in this lesson.
Why not just use "Apply to Each"? You could loop through every item and manually build a new object inside an Apply to Each block. But that approach creates extra actions, makes your flow harder to read, and is significantly slower on large datasets. Select and Filter Array are purpose-built for transformation — they do the work in a single action.
The Compose action is the simplest of the three, and that simplicity is what makes it powerful. It takes whatever you put into its Inputs field — a value, an expression, a piece of text, an entire object — and stores it so you can reference it later in your flow using the outputs('Compose') dynamic value.
Think of Compose as a sticky note you write at the top of your desk. You calculate something once, write it down, and then refer to that note as many times as you need without recalculating it.
Power Automate doesn't have a native "variable assignment" inside every expression. If you need to use the same computed value — say, today's date formatted a specific way — in five different places, you'd normally have to rewrite that expression five times. With Compose, you write it once.
Let's say you want to capture today's date in a readable format to use later in email subjects and file names. Here's how you'd set that up:
formatDateTime(utcNow(), 'yyyy-MM-dd')
Now anywhere downstream in your flow, when you add dynamic content, you can select Outputs from this Compose action and it will carry that formatted date string.
You can also use Compose to construct a complete JSON object that you'll pass to another action:
{
"report_title": "Daily Pending Orders",
"generated_on": "2024-03-15",
"generated_by": "Power Automate"
}
Just type or paste that directly into the Inputs field. Compose will store it as-is.
Tip: Name your Compose actions descriptively. Instead of the default "Compose," rename it to something like "Compose — Formatted Today's Date" or "Compose — Report Metadata." When you have six Compose actions in a flow, the default names become impossible to navigate.
If Compose is a sticky note, Select is a cookie cutter. You give it an array, define the shape you want each item to come out as, and it applies that shape to every single item. The result is a brand-new array with the same number of rows, but only the fields you chose — renamed and restructured however you like.
This is the equivalent of SELECT order_id, customer_name, total_amount FROM orders in SQL. You're not filtering rows; you're choosing and renaming columns.
The Select action has two inputs:
Add a new step and search for "Select" (also under Data Operation). You'll see two fields: From and Map.
Click into From and select your array — for our scenario, this is the dynamic value representing the full list of orders returned by the API call.
Now for the Map section, you'll see a small grid with a left column (key/name) and a right column (value). Let's build our three-field output.
Row 1:
order_iditem()['order_id']Row 2:
customer_nameitem()['customer_name']Row 3:
order_totalitem()['total_amount']Notice that third row — we renamed total_amount to order_total. That's intentional. The key column is your output name, and you control it completely.
What is
item()? Inside a Select action's Map section,item()refers to the current item being processed — the equivalent of "this row" as Select loops through the array. You access its fields using bracket notation:item()['field_name'].
Select isn't just a rename tool — you can apply expressions to transform the values themselves. Want the total formatted as currency? Use:
formatNumber(item()['total_amount'], 'C2', 'en-US')
Want the customer name in uppercase?
toUpper(item()['customer_name'])
Want to concatenate a label onto the order ID?
concat('Order #', item()['order_id'])
The Map section accepts any valid Power Automate expression in the value column. This is where Select earns its keep — you're not just selecting fields, you're shaping data.
After running Select on our sample array, the output would be a new array like this:
[
{
"order_id": "ORD-1041",
"customer_name": "Priya Sharma",
"order_total": 142.50
},
{
"order_id": "ORD-1042",
"customer_name": "Marcus Webb",
"order_total": 89.00
},
{
"order_id": "ORD-1043",
"customer_name": "Leila Okonkwo",
"order_total": 310.00
}
]
Three rows, three fields, clean and simple. All three items made it through because Select doesn't filter — it only transforms.
Now we tackle the filtering problem. Filter Array takes an array and applies a condition to each item. Items that meet the condition stay; items that don't are removed. The shape of each item doesn't change — that's Select's job. Filter Array purely decides which rows survive.
Think of it as a bouncer at a club door. Every item in your array walks up to the door. The bouncer checks the condition. Pass? You're in. Fail? You're not on the list.
Filter Array offers two ways to define your condition:
Basic mode gives you a simple three-part interface: left value, comparison operator (equals, does not equal, greater than, etc.), and right value. This works for simple, single-condition filters.
Advanced mode (click "Edit in advanced mode") lets you write a full logical expression using Power Automate's formula language. This is what you'll use for anything beyond a single equals comparison.
Add a Filter Array action. In the From field, select your input array.
For a basic filter to keep only orders where status equals "pending":
item()['status']pending (no quotes needed in the basic mode value field)That's it. Run the flow and only the pending orders pass through.
What if you want orders that are pending AND have a total over $100? Basic mode can't handle AND logic. Switch to advanced mode and write:
@and(
equals(item()['status'], 'pending'),
greater(item()['total_amount'], 100)
)
The @ prefix tells Power Automate this is an expression. The and() function takes two conditions, both of which must be true for the item to pass.
Warning: In advanced mode, string values in expressions must use single quotes, not double quotes. Writing
equals(item()['status'], "pending")will fail. Use'pending'instead.
You can also use or() for either-or conditions:
@or(
equals(item()['status'], 'pending'),
equals(item()['status'], 'processing')
)
Applying the basic status filter to our sample array, the output would be:
[
{
"order_id": "ORD-1041",
"customer_name": "Priya Sharma",
"status": "pending",
"total_amount": 142.50,
"internal_warehouse_code": "WH-7",
"created_date": "2024-03-15T08:22:00Z"
},
{
"order_id": "ORD-1043",
"customer_name": "Leila Okonkwo",
"status": "pending",
"total_amount": 310.00,
"internal_warehouse_code": "WH-7",
"created_date": "2024-03-15T11:00:00Z"
}
]
Marcus Webb's shipped order is gone. The remaining items still have all their original fields — Filter Array didn't reshape anything. That's why order matters when you chain these two actions together.
Here's the key insight about using these actions together: order matters. Should you filter first or select first?
Filter first, then Select. Here's why: Filter Array uses item()['field_name'] to evaluate conditions. If you run Select first and strip out the status field, your Filter Array action can no longer access it to evaluate the condition. You'd be trying to check a field that no longer exists.
The correct chain for our scenario:
status equals pendingorder_id, customer_name, order_totalThis is clean, efficient, and readable. Anyone opening your flow can immediately understand what's happening at each stage.
Tip: Use a Compose action between steps to inspect intermediate state during development. Add a Compose action after Filter Array and set its input to the Filter Array output. Run the flow manually, then open the run history and expand that Compose action to see exactly what the array looks like at that point. Delete the Compose action once you're done debugging.
Build this flow from start to finish. It uses a manual trigger so you don't need any external system to practice.
Goal: Start with a hardcoded array of employee records. Filter to only active employees in the Engineering department. Then map them to a simplified output with just their name, department, and a formatted employee label.
Go to My Flows → New Flow → Instant cloud flow → Manually trigger a flow.
Add a Compose step. In the Inputs field, paste this array directly:
[
{"emp_id": "E101", "full_name": "Jordan Kim", "department": "Engineering", "status": "active", "salary": 95000},
{"emp_id": "E102", "full_name": "Sandra Osei", "department": "Marketing", "status": "active", "salary": 72000},
{"emp_id": "E103", "full_name": "Tobias Reyes", "department": "Engineering", "status": "inactive", "salary": 88000},
{"emp_id": "E104", "full_name": "Anika Patel", "department": "Engineering", "status": "active", "salary": 102000}
]
Rename this step to "Compose — Sample Employee Data".
Add a Filter Array step. Set the From field to the outputs of your Compose step (select it from dynamic content).
Switch to advanced mode and enter:
@and(
equals(item()['status'], 'active'),
equals(item()['department'], 'Engineering')
)
This keeps only Jordan Kim and Anika Patel.
Add a Select step. Set From to the output of the Filter Array step.
In the Map section, add these three rows:
| Key | Value (Expression) |
|---|---|
employee_label |
concat('EMP: ', item()['full_name']) |
department |
item()['department'] |
status |
toUpper(item()['status']) |
Add one more Compose step and set its input to the output of the Select action. This lets you inspect the final array in the run history.
Click Save, then click Test → Manually → Run flow. Once it completes, click on the run to open the run history, expand the final Compose action, and inspect the outputs. You should see:
[
{
"employee_label": "EMP: Jordan Kim",
"department": "Engineering",
"status": "ACTIVE"
},
{
"employee_label": "EMP: Anika Patel",
"department": "Engineering",
"status": "ACTIVE"
}
]
"My Select output is an array of arrays, not an array of objects."
This happens when the From field of Select is set to the entire output of an action instead of the array body within it. For example, if an HTTP action returns a response with a body property containing your array, you need to reference body specifically, not the whole HTTP response. Use body('HTTP_Action_Name') in an expression.
"Filter Array is keeping everything / filtering everything out."
Check your field name capitalization. item()['Status'] is different from item()['status']. JSON field names are case-sensitive. Open your run history, find the trigger or input action, and look at the exact field names in the raw data.
"My expression in advanced Filter mode throws an error about invalid JSON."
Make sure you're using single quotes for string literals, not double quotes. Also ensure you haven't accidentally left the @ sign out — without it, the field is treated as a plain string, not an expression.
"I renamed my Compose action and now downstream references are broken." When you rename an action, any dynamic content references that were already placed in later steps may break because they still reference the old internal name. After renaming, manually re-select the dynamic content outputs in the downstream steps to update the references.
"Select is outputting null for some fields."
The field name in your item()['field_name'] expression doesn't match what's actually in the data. Again, case matters. Use run history to inspect the raw input to Select and copy the field names exactly as they appear.
You've now got the three foundational data transformation actions in your Power Automate toolkit. Let's recap what each one does:
The pattern that makes these powerful is combining them in the right order: filter first to remove unwanted rows, then select to reshape what remains. This approach keeps your conditions clear (they can reference all original fields) and keeps your data clean at the end.
Where to go next:
item()['field_name'] expressions everywhere. It's the natural companion to these three actions.Master these three actions and you'll handle the data transformation layer of almost any flow without fighting with the tool. The goal is always the same: get the right data, in the right shape, to the right destination — and now you know exactly how to do that.
Learning Path: Flow Automation Basics