Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Mastering Dynamic Expressions and the Power Automate Formula Language: String, Date, and Array Functions for Real-World Data Manipulation

Mastering Dynamic Expressions and the Power Automate Formula Language: String, Date, and Array Functions for Real-World Data Manipulation

Power Automate⚡ Practitioner21 min readJul 3, 2026Updated Jul 3, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • The Expression Editor: A Brief Orientation Before We Go Deep
  • String Functions: Cleaning and Reshaping Text Data
  • The Customer Name Normalization Problem
  • `toLower`, `toUpper`, and the Title Case Gap
  • Extracting Name Parts with `indexOf` and `substring`
  • Email Cleaning with `trim` and `toLower`
  • Using `substring` and `indexOf` for Targeted Extraction
  • `startsWith`, `endsWith`, and `contains` for Conditional Logic
  • Date Functions: Time Is More Complicated Than It Looks
  • The Date Format Chaos Problem
  • `utcNow()` and `formatDateTime()`
  • `addDays`, `addHours`, `addMinutes`, `addSeconds`
  • `ticks()` and Date Arithmetic
  • Parsing Incoming Date Strings
  • Array Functions: Working with Collections of Data
  • Why Arrays Are the Heart of Real Flows
  • `length()`, `first()`, `last()`, and Direct Indexing
  • `filter()`: The Most Powerful Array Function
  • `select()`: Reshaping Array Items
  • `join()`: Turning Arrays into Strings
  • `union()`, `intersection()`, and Deduplication
  • `contains()` with Arrays
  • Chaining It All Together: The Compose Action Pattern
  • Hands-On Exercise: Building a Customer Record Normalizer
  • Common Mistakes and Troubleshooting
  • "InvalidTemplate" Error
  • "The template language expression is not valid" with a long path
  • `filter()` Returns an Empty Array When You Expected Results
  • `ticks()` Fails on Date-Only Strings
  • `select()` Inside Apply to Each Causes Confusion
  • Numbers That Look Like Numbers but Aren't
  • Performance Implications: When Not to Use Expression-Based Transforms
  • Summary and Next Steps
  • Mastering Dynamic Expressions and the Power Automate Formula Language: String, Date, and Array Functions for Real-World Data Manipulation

    Introduction

    You've built flows that move data from point A to point B. You've set up triggers, added conditions, sent emails. But now you're hitting the wall that every Power Automate practitioner hits eventually: the data coming in doesn't look like the data you need going out. Dates arrive in ISO format but your downstream system wants MM/DD/YYYY. Names come in as JOHN SMITH in all caps. Arrays of customer records need to be filtered and reshaped before you can do anything useful with them. The forms people submit contain trailing spaces, inconsistent casing, and phone numbers with every conceivable combination of dashes, dots, and parentheses.

    This is where the Power Automate expression language — built on the Azure Logic Apps workflow expression syntax — transforms from a curiosity into a superpower. Most practitioners click through the dynamic content panel and grab values by name, which works fine until it doesn't. When you need to transform data rather than just pass it along, you need to drop into the expression editor and write actual function calls. The expression language covers string manipulation, date arithmetic, array querying, type conversion, and logical operations, and it does all of it inline without requiring a separate "Run a Script" step or a call to an external API.

    By the end of this lesson, you will be able to reshape incoming data into whatever format your downstream systems require, entirely within your flow. We'll work through realistic, production-grade scenarios — a customer data normalization flow, a reporting pipeline that calculates business-relevant date ranges, and an array transformation that extracts and reshapes records from a complex JSON payload.

    What you'll learn:

    • How to use string functions (concat, substring, replace, toLower, trim, split, indexOf) to clean and reformat text data
    • How to perform date manipulation (addDays, formatDateTime, utcNow, ticks, dateDifference) for scheduling and reporting scenarios
    • How to work with arrays using filter, select, first, last, join, union, intersection, and length
    • How to chain expressions together and store intermediate results with Compose actions
    • How to debug expression errors and interpret the cryptic messages Power Automate gives you

    Prerequisites

    You should already understand Power Automate's basic building blocks: triggers, actions, conditions, Apply to Each loops, and the dynamic content panel. You should be comfortable opening the expression editor (the fx button in any value field) and have a working knowledge of JSON — you'll be reading and writing JSON throughout this lesson. If you've built at least a handful of flows in production and found yourself wanting more control over data transformation, you're in the right place.


    The Expression Editor: A Brief Orientation Before We Go Deep

    When you click into any value field in Power Automate and switch to the Expression tab (rather than Dynamic Content), you're entering the workflow expression language. Every expression evaluates to a single value. That value can be a string, a number, a boolean, an array, or an object. You wrap expressions in @{...} when embedding them inside a string template (like an email body), or you use them bare as the entire value of a field.

    A few things that trip people up early:

    • Expressions are not JavaScript, Python, or M (Power Query). Some functions look similar, but the behavior can differ.
    • String concatenation uses the concat() function, not +.
    • Array indexes are zero-based: array[0] is the first element.
    • You can nest function calls as deeply as you need to — trim(toLower(outputs('Get_row')?['CustomerName'])) is a perfectly valid expression.

    The best way to experiment with expressions before deploying them is to add a Compose action, put your expression in the Inputs field, and run the flow manually. The Compose output shows you exactly what the expression evaluated to. Think of it as your expression REPL.


    String Functions: Cleaning and Reshaping Text Data

    The Customer Name Normalization Problem

    Let's start with a realistic scenario. You're pulling customer records from a legacy CRM that stores names in all caps (because the system was built in 1994). The records look like this:

    {
      "CustomerName": "JOHNSON, MARY ELIZABETH",
      "Email": "  MJOHNSON@EXAMPLE.COM  ",
      "Phone": "(555) 867-5309"
    }
    

    Your modern CRM expects first name and last name as separate fields, proper casing, a cleaned email, and a phone number in +15558675309 format. Let's walk through building these transformations.

    `toLower`, `toUpper`, and the Title Case Gap

    Power Automate gives you toLower() and toUpper(), but infuriatingly, there is no built-in toTitleCase(). You'll encounter this limitation constantly. Here's the workaround for a single-word name:

    concat(toUpper(substring(toLower('mary'), 0, 1)), substring(toLower('mary'), 1))
    

    This takes the first character, uppercases it, then appends the rest of the string in lowercase. The result is Mary.

    For our compound JOHNSON, MARY ELIZABETH case, you'd need to split on spaces and apply this to each word, which is genuinely awkward in the expression language alone. In practice, this is a good candidate for a child flow or a small Azure Function. For the lesson, we'll use a simpler approach: split on the comma first to separate last and first names.

    // Get last name (everything before the comma)
    trim(toLower(first(split(outputs('Compose_RawName'), ','))))
    
    // Get first name (everything after the comma)
    trim(toLower(last(split(outputs('Compose_RawName'), ','))))
    

    split() takes a string and a delimiter and returns an array. first() and last() pull the first and last elements of that array. trim() removes leading and trailing whitespace. toLower() converts to lowercase. Chained together in a single expression, this is readable enough and does the job.

    Tip: Store your raw incoming values in Compose actions with descriptive names like Compose_RawName. This makes your expressions shorter and your flow easier to read. Long chains of triggerOutputs()?['body']?['CustomerName'] buried inside a complex expression become nightmares to debug six months later.

    Extracting Name Parts with `indexOf` and `substring`

    Sometimes you don't have a clean delimiter. Let's say the phone field is (555) 867-5309 and you need just the digits. There's no built-in "strip non-alphanumeric" function, but you can build the logic with replace():

    replace(replace(replace(replace(replace(outputs('Compose_RawPhone'), '(', ''), ')', ''), ' ', ''), '-', ''), '+', '')
    

    This nests five replace() calls, each stripping one character type. The result is 5558675309. Then prepend the country code:

    concat('+1', replace(replace(replace(replace(replace(outputs('Compose_RawPhone'), '(', ''), ')', ''), ' ', ''), '-', ''), '+', ''))
    

    Result: +15558675309.

    Yes, this looks ugly. That's the honest reality of the expression language — it lacks regex, so complex string cleaning requires either nesting or a multi-step Compose chain where each step outputs a progressively cleaner string.

    Email Cleaning with `trim` and `toLower`

    The email field MJOHNSON@EXAMPLE.COM needs trimming and lowercasing:

    trim(toLower(outputs('Compose_RawEmail')))
    

    Result: mjohnson@example.com. Simple and clean. This is the best-case scenario for string transformation.

    Using `substring` and `indexOf` for Targeted Extraction

    substring(text, startIndex, count) extracts a portion of a string. indexOf(text, searchString) tells you where a substring begins (returns -1 if not found). Together, they let you extract content between known delimiters.

    Suppose you receive order IDs like ORD-2024-00847-EMEA and you need just the numeric portion 00847. The pattern is: the number starts after the third dash and ends before the fourth dash.

    // Find the position of the third dash by working through the string
    // First, find where '2024' ends (position of second dash + 5 for '2024-')
    // This is why Compose chaining matters
    
    // Compose_SecondDashPos:
    add(indexOf(outputs('Compose_OrderID'), '-'), 
        add(indexOf(substring(outputs('Compose_OrderID'), 
                              add(indexOf(outputs('Compose_OrderID'), '-'), 1), 
                              100), '-'), 2))
    

    This is getting unwieldy. Here's the honest lesson: when your string manipulation requires more than three to four levels of nesting, break it into multiple Compose steps. Name them Compose_Step1_FirstDash, Compose_Step2_SecondDash, and so on. Your future self will thank you.

    A more practical approach for the order ID example: since you know the format is always ORD-YYYY-NNNNN-REGION, use split():

    // Split on '-' gives ['ORD', '2024', '00847', 'EMEA']
    // Take the element at index 2
    outputs('Compose_OrderIDParts')[2]
    

    Where Compose_OrderIDParts contains split(outputs('Compose_OrderID'), '-').

    Warning: indexOf() returns the position of the first occurrence of the search string. If your delimiter appears multiple times, you may get unexpected results. split() is almost always cleaner when you have consistent delimiters.

    `startsWith`, `endsWith`, and `contains` for Conditional Logic

    These three functions are indispensable in condition branches:

    // Check if an email is from a corporate domain
    startsWith(outputs('Compose_Email'), 'admin@')
    
    // Check if an order ID is from EMEA
    endsWith(outputs('Compose_OrderID'), 'EMEA')
    
    // Check if a description mentions a specific product
    contains(outputs('Compose_Description'), 'Enterprise License')
    

    These return booleans, so they go directly into Condition actions or if() expressions.


    Date Functions: Time Is More Complicated Than It Looks

    The Date Format Chaos Problem

    Dates in real systems are a mess. You'll receive ISO 8601 timestamps, Unix epoch integers, MM/DD/YYYY strings, and occasionally something like 20240315 (compact format used in some EDI systems). You need to normalize them, do arithmetic, and emit them in whatever format your downstream system expects.

    `utcNow()` and `formatDateTime()`

    utcNow() gives you the current UTC timestamp in ISO 8601 format: 2024-03-15T14:32:00.0000000Z. You'll use it constantly.

    formatDateTime(timestamp, format) converts a timestamp to a formatted string. The format string uses .NET custom date format specifiers:

    // Today's date as YYYY-MM-DD
    formatDateTime(utcNow(), 'yyyy-MM-dd')
    // Result: 2024-03-15
    
    // Friendly format for email subjects
    formatDateTime(utcNow(), 'MMMM d, yyyy')
    // Result: March 15, 2024
    
    // With time, 12-hour format
    formatDateTime(utcNow(), 'MM/dd/yyyy h:mm tt')
    // Result: 03/15/2024 2:32 PM
    

    Tip: yyyy (lowercase) is four-digit year. YYYY is ISO week year, which behaves differently at year boundaries. Always use yyyy unless you specifically need ISO week numbering.

    `addDays`, `addHours`, `addMinutes`, `addSeconds`

    These functions shift a timestamp by a given amount. They all follow the same signature: addDays(timestamp, numberOfDays). Use negative numbers to go backward.

    Scenario: Your flow generates weekly reports. You need the start and end of the previous week (Monday to Sunday), regardless of what day the flow runs.

    // Day of week (0=Sunday, 1=Monday, ..., 6=Saturday)
    // dayOfWeek() returns an integer
    dayOfWeek(utcNow())
    
    // Days since last Monday:
    // If today is Wednesday (3), we need to go back 2 days
    // Formula: subtract (dayOfWeek - 1) days, but handle Sunday (0) as 7
    // For Monday as week start:
    // daysToSubtract = if(dayOfWeek == 0, 6, dayOfWeek - 1)
    if(equals(dayOfWeek(utcNow()), 0), 
       addDays(utcNow(), -6), 
       addDays(utcNow(), mul(sub(dayOfWeek(utcNow()), 1), -1)))
    

    Store that in Compose_ThisMonday, then:

    // Previous Monday (start of last week)
    addDays(outputs('Compose_ThisMonday'), -7)
    
    // Previous Sunday (end of last week)
    addDays(outputs('Compose_ThisMonday'), -1)
    

    Format these for your report:

    formatDateTime(addDays(outputs('Compose_ThisMonday'), -7), 'yyyy-MM-dd')
    // Result: 2024-03-04 (if today is in the week of March 11)
    

    `ticks()` and Date Arithmetic

    Sometimes you need to calculate the difference between two dates in days, hours, or minutes. Power Automate doesn't have a direct dateDiff() function (Logic Apps does, but it hasn't fully propagated to Power Automate everywhere). The workaround uses ticks().

    ticks() converts a timestamp to the number of 100-nanosecond intervals since January 1, 0001. The difference in ticks divided by the right constant gives you the duration you need.

    // Ticks per day: 864000000000
    // Ticks per hour: 36000000000
    // Ticks per minute: 600000000
    
    // Days between two dates:
    div(
      sub(
        ticks(outputs('Compose_EndDate')),
        ticks(outputs('Compose_StartDate'))
      ),
      864000000000
    )
    

    Scenario: You have a subscription renewal date stored in a SharePoint list column. You want to flag customers whose subscription expires within the next 30 days.

    // Is renewal within 30 days?
    less(
      div(
        sub(
          ticks(item()?['RenewalDate']),
          ticks(utcNow())
        ),
        864000000000
      ),
      30
    )
    

    This expression returns true if the renewal date is less than 30 days away. Plug it directly into a Condition action.

    Warning: ticks() expects a valid ISO 8601 timestamp. If your date column contains a date-only value like 2024-03-15, you may need to convert it: ticks(concat(item()?['RenewalDate'], 'T00:00:00Z')). Always verify your date format before using it with ticks().

    Parsing Incoming Date Strings

    If a system sends you a date as 20240315 (compact format), you need to reconstruct it before any date functions will work:

    // Convert 20240315 to 2024-03-15T00:00:00Z
    concat(
      substring(outputs('Compose_CompactDate'), 0, 4), '-',
      substring(outputs('Compose_CompactDate'), 4, 2), '-',
      substring(outputs('Compose_CompactDate'), 6, 2), 'T00:00:00Z'
    )
    

    For MM/DD/YYYY input:

    // Convert 03/15/2024 to 2024-03-15T00:00:00Z
    concat(
      last(split(outputs('Compose_USDate'), '/')), '-',
      first(split(outputs('Compose_USDate'), '/')), '-',
      outputs('Compose_SplitDate')[1], 'T00:00:00Z'
    )
    

    Here, Compose_SplitDate holds split(outputs('Compose_USDate'), '/'), giving ['03', '15', '2024']. Element [1] is 15 (the day). This is cleaner when broken across Compose steps.


    Array Functions: Working with Collections of Data

    Why Arrays Are the Heart of Real Flows

    Most real-world automation involves collections: lists of orders, arrays of approval results, tables of customer records, sets of tags. The difference between a practitioner who writes clean, maintainable flows and one who writes fragile spaghetti is almost always how they handle arrays.

    `length()`, `first()`, `last()`, and Direct Indexing

    The fundamentals:

    // Count items in an array
    length(outputs('Compose_OrderArray'))
    
    // Get the first item
    first(outputs('Compose_OrderArray'))
    
    // Get the last item
    last(outputs('Compose_OrderArray'))
    
    // Get the item at index 2 (third item)
    outputs('Compose_OrderArray')[2]
    
    // Access a property of the item at index 2
    outputs('Compose_OrderArray')[2]?['OrderTotal']
    

    The ? before ['PropertyName'] is the null-conditional operator. It prevents the entire expression from failing if the property doesn't exist on that item — it returns null instead of an error. Use it defensively.

    `filter()`: The Most Powerful Array Function

    filter(array, expression) returns a new array containing only the items for which the expression evaluates to true. This is your primary tool for querying arrays.

    Scenario: You've retrieved a list of all support tickets from a REST API. The array looks like this:

    [
      {"id": "TKT-001", "status": "open", "priority": "high", "region": "EMEA", "age_days": 5},
      {"id": "TKT-002", "status": "closed", "priority": "high", "region": "EMEA", "age_days": 12},
      {"id": "TKT-003", "status": "open", "priority": "low", "region": "AMER", "age_days": 2},
      {"id": "TKT-004", "status": "open", "priority": "high", "region": "EMEA", "age_days": 8}
    ]
    

    You want only the open, high-priority EMEA tickets. In filter(), you reference the current item with item():

    filter(
      outputs('Compose_AllTickets'),
      and(
        equals(item()?['status'], 'open'),
        and(
          equals(item()?['priority'], 'high'),
          equals(item()?['region'], 'EMEA')
        )
      )
    )
    

    Result:

    [
      {"id": "TKT-001", "status": "open", "priority": "high", "region": "EMEA", "age_days": 5},
      {"id": "TKT-004", "status": "open", "priority": "high", "region": "EMEA", "age_days": 8}
    ]
    

    Warning: filter() in Power Automate expressions is not the same as the OData $filter query parameter you use in SharePoint or Dataverse actions. The expression filter() runs in the flow runtime against an in-memory array. For large datasets, always filter at the source using query parameters before loading into flow memory — filter() on a 10,000-row array will work, but it's wasteful and slow.

    `select()`: Reshaping Array Items

    select(array, expression) transforms every item in an array, returning a new array of the same length where each item is the result of the expression applied to the original item.

    Scenario: You only need id and age_days from the filtered tickets, not the full record, because you're sending this to a downstream API that doesn't accept extra fields.

    select(
      outputs('Compose_FilteredTickets'),
      item()?['id']
    )
    

    This returns just the IDs: ["TKT-001", "TKT-004"].

    For reshaping into new objects, use select() with an object expression:

    select(
      outputs('Compose_FilteredTickets'),
      createObject(
        'ticketId', item()?['id'],
        'daysOpen', item()?['age_days'],
        'urgencyLabel', if(greater(item()?['age_days'], 7), 'ESCALATE', 'MONITOR')
      )
    )
    

    Result:

    [
      {"ticketId": "TKT-001", "daysOpen": 5, "urgencyLabel": "MONITOR"},
      {"ticketId": "TKT-004", "daysOpen": 8, "urgencyLabel": "ESCALATE"}
    ]
    

    Notice the inline if() expression that computes the urgency label based on age. This is the compositional power of the expression language: you can embed any expression inside select(), including other function calls.

    Tip: createObject() was added to Power Automate expressions relatively recently. If you're working with older flows or see documentation using JSON string construction with concat(), createObject() is the cleaner modern approach.

    `join()`: Turning Arrays into Strings

    join(array, delimiter) concatenates all elements of an array into a single string with the specified delimiter between elements.

    // Convert array of IDs to comma-separated string for an email
    join(outputs('Compose_EscalatedIDs'), ', ')
    // Result: "TKT-004"
    
    // All IDs
    join(select(outputs('Compose_AllTickets'), item()?['id']), ' | ')
    // Result: "TKT-001 | TKT-002 | TKT-003 | TKT-004"
    

    This is particularly useful for building email bodies, generating query strings, and creating display-friendly summaries from array data.

    `union()`, `intersection()`, and Deduplication

    union(array1, array2) returns a new array containing all unique elements from both arrays. Despite the name, it also functions as a deduplication tool when called with two identical arrays:

    // Deduplicate an array of tags
    union(outputs('Compose_Tags'), outputs('Compose_Tags'))
    // If input is ['crm', 'sales', 'crm', 'enterprise'], output is ['crm', 'sales', 'enterprise']
    

    intersection(array1, array2) returns only the elements present in both arrays:

    // Which tags exist in both the new record and the approved list?
    intersection(
      outputs('Compose_SubmittedTags'),
      outputs('Compose_ApprovedTags')
    )
    

    `contains()` with Arrays

    contains() works on arrays too, not just strings. It checks whether an array includes a specific value:

    // Is 'enterprise' in the tags array?
    contains(outputs('Compose_Tags'), 'enterprise')
    // Returns true or false
    

    For checking whether an array of objects contains an item with a specific property value, you'd combine filter() and length():

    // Does any ticket have id 'TKT-003'?
    greater(
      length(filter(outputs('Compose_AllTickets'), equals(item()?['id'], 'TKT-003'))),
      0
    )
    

    Chaining It All Together: The Compose Action Pattern

    The secret to maintainable, debuggable expression logic is what I call the Compose Chain Pattern. Rather than writing one enormous nested expression, you build your transformation incrementally, storing each intermediate result in a named Compose action.

    Here's the pattern for the order ID extraction scenario we mentioned earlier:

    1. Compose_RawOrderID — stores triggerOutputs()?['body']?['order_id']
    2. Compose_OrderIDParts — stores split(outputs('Compose_RawOrderID'), '-')
    3. Compose_OrderNumber — stores outputs('Compose_OrderIDParts')[2]
    4. Compose_Region — stores outputs('Compose_OrderIDParts')[3]
    5. Compose_OrderYear — stores int(outputs('Compose_OrderIDParts')[1])

    Each step is named, each output is visible in the run history, and each one can be referenced by name in any subsequent step. When something breaks, you look at the run history, find the first Compose that has the wrong value, and you've immediately isolated the problem.

    This pattern costs you a few extra actions, but the debugging and maintenance benefits are substantial. In production flows that other team members might need to troubleshoot, this is not optional — it's professional practice.


    Hands-On Exercise: Building a Customer Record Normalizer

    Here's a complete exercise that puts everything together. You'll build a flow that receives a webhook payload of customer records, normalizes them, and outputs a clean JSON array ready for database insertion.

    Input payload (from an HTTP trigger):

    {
      "customers": [
        {
          "raw_name": "CHEN, DAVID WEI",
          "email": "  DCHEN@ACMECORP.COM  ",
          "phone": "(415) 555-0192",
          "signup_date": "20231108",
          "plan": "enterprise",
          "tags": ["sales", "enterprise", "sales", "west-coast"]
        },
        {
          "raw_name": "OKONKWO, AMARA",
          "email": "AOKONKWO@GLOBALTECH.IO",
          "phone": "800-555-0147",
          "signup_date": "20240215",
          "plan": "professional",
          "tags": ["marketing", "professional"]
        },
        {
          "raw_name": "PATEL, SUNITA RANI",
          "email": "  SPATEL@STARTUPXYZ.COM",
          "phone": "(312)555-0273",
          "signup_date": "20240301",
          "plan": "enterprise",
          "tags": ["sales", "enterprise", "midwest"]
        }
      ]
    }
    

    Your task: Build a flow that produces a normalized array where:

    • last_name and first_name are split from raw_name, trimmed, and lowercased
    • email is trimmed and lowercased
    • phone is stripped to digits and prefixed with +1
    • signup_date is converted to ISO format (YYYY-MM-DD)
    • days_since_signup is calculated
    • tags are deduplicated
    • Only enterprise plan customers are included

    Step-by-step build:

    1. Trigger: HTTP with JSON schema matching the input payload above. Enable schema generation by pasting the sample payload.

    2. Compose_AllCustomers: triggerOutputs()?['body']?['customers']

    3. Compose_EnterpriseOnly:

      filter(outputs('Compose_AllCustomers'), equals(item()?['plan'], 'enterprise'))
      
    4. Compose_Normalized:

      select(
        outputs('Compose_EnterpriseOnly'),
        createObject(
          'last_name', trim(toLower(first(split(item()?['raw_name'], ',')))),
          'first_name', trim(toLower(last(split(item()?['raw_name'], ',')))),
          'email', trim(toLower(item()?['email'])),
          'phone', concat('+1', replace(replace(replace(replace(item()?['phone'], '(', ''), ')', ''), '-', ''), ' ', '')),
          'signup_date', concat(substring(item()?['signup_date'], 0, 4), '-', substring(item()?['signup_date'], 4, 2), '-', substring(item()?['signup_date'], 6, 2)),
          'days_since_signup', div(sub(ticks(utcNow()), ticks(concat(substring(item()?['signup_date'], 0, 4), '-', substring(item()?['signup_date'], 4, 2), '-', substring(item()?['signup_date'], 6, 2), 'T00:00:00Z'))), 864000000000),
          'tags', union(item()?['tags'], item()?['tags'])
        )
      )
      
    5. Response: Return outputs('Compose_Normalized') as the HTTP response body with Content-Type: application/json.

    Expected output:

    [
      {
        "last_name": "chen",
        "first_name": "david wei",
        "email": "dchen@acmecorp.com",
        "phone": "+14155550192",
        "signup_date": "2023-11-08",
        "days_since_signup": 127,
        "tags": ["sales", "enterprise", "west-coast"]
      },
      {
        "last_name": "patel",
        "first_name": "sunita rani",
        "email": "spatel@startupxyz.com",
        "phone": "+13125550273",
        "signup_date": "2024-03-01",
        "days_since_signup": 14,
        "tags": ["sales", "enterprise", "midwest"]
      }
    ]
    

    Notice that Okonkwo is excluded (professional plan), Chen's duplicate "sales" tag is removed, and all values are clean.


    Common Mistakes and Troubleshooting

    "InvalidTemplate" Error

    This is Power Automate's most cryptic error. It means your expression has a syntax error. Common causes:

    • Mismatched parentheses. Count opening and closing parentheses. For complex expressions, write them in a text editor with bracket matching before pasting.
    • Wrong quote type. The expression editor uses straight single quotes '. If you copy-paste from Word or some web pages, you may get curly quotes ' which break everything.
    • Missing commas between arguments. substring(text 0 4) instead of substring(text, 0, 4).

    "The template language expression is not valid" with a long path

    This usually means you're trying to access a property on a null value. Add null checks:

    // Risky:
    item()?['Address']?['City']
    
    // Safer in a condition:
    if(empty(item()?['Address']), 'Unknown', item()?['Address']?['City'])
    

    `filter()` Returns an Empty Array When You Expected Results

    The most common cause is a type mismatch. If your array contains "age": "5" (string) and you write greater(item()?['age'], 7), the comparison fails silently because you're comparing a string to an integer. Convert explicitly:

    greater(int(item()?['age']), 7)
    

    Similarly, equals(item()?['status'], 'open') is case-sensitive. OPEN does not equal open. Add toLower() defensively:

    equals(toLower(item()?['status']), 'open')
    

    `ticks()` Fails on Date-Only Strings

    Covered earlier, but worth repeating: ticks('2024-03-15') will often fail. Use ticks('2024-03-15T00:00:00Z') instead.

    `select()` Inside Apply to Each Causes Confusion

    If you use select() inside an Apply to Each loop, the item() function inside select() refers to the select() array item, not the Apply to Each loop item. You lose access to the outer loop context. The solution is to compute the array using select() in a Compose action before the loop, or use variables to capture outer loop values before passing them into nested expressions.

    Numbers That Look Like Numbers but Aren't

    APIs frequently return numeric values as strings in JSON. "OrderTotal": "1250.00" looks like a number but is a string. Arithmetic operations will fail or return unexpected results. Use float(item()?['OrderTotal']) or int(item()?['OrderTotal']) to convert before doing math.


    Performance Implications: When Not to Use Expression-Based Transforms

    Expression-based transformation runs in the flow runtime and is billed as part of your action executions. For small to medium arrays (say, under a few hundred items), filter() and select() in expressions are perfectly appropriate and significantly more efficient than using Apply to Each loops with conditional steps.

    However, for large arrays — thousands of items — the memory and execution overhead can cause timeouts or throttling. In those cases:

    • Filter at the source: Use OData $filter for SharePoint/Dataverse, SQL WHERE clauses, or API query parameters to reduce payload size before it enters your flow.
    • Consider Azure Functions: For CPU-intensive transformation logic (regex, complex string parsing, large array operations), an HTTP-triggered Azure Function called from Power Automate gives you a proper programming language with real performance characteristics.
    • Use Dataverse computed columns or SQL views: If you're repeatedly reshaping the same data from the same source, the transformation might belong at the data layer rather than in every flow that reads it.

    The expression language is powerful, but it's not a data processing engine. Use it for lightweight shaping and enrichment; delegate heavy lifting to appropriate infrastructure.


    Summary and Next Steps

    You've moved well past "drag-and-drop" automation. Here's what you now have in your toolkit:

    String functions: concat, substring, indexOf, replace, toLower, toUpper, trim, split, startsWith, endsWith, contains — enough to clean and reformat virtually any text data you'll encounter.

    Date functions: utcNow, formatDateTime, addDays, addHours, ticks, dayOfWeek — enough to handle scheduling logic, duration calculations, and cross-format date normalization.

    Array functions: filter, select, first, last, join, union, intersection, length, contains — enough to query, reshape, and aggregate collections without writing a single loop.

    The Compose Chain Pattern: Break complex transformations into named intermediate steps for debuggability and team readability.

    Where to go next:

    • Explore xpath() and json() functions for working with XML payloads and parsing raw JSON strings — essential if you're integrating with SOAP APIs or legacy systems.
    • Learn variables vs. Compose actions and when mutable state is worth the complexity.
    • Study error handling with try/catch patterns using scope actions and the result() function — production flows need resilience, not just happy-path success.
    • Investigate child flows and input/output schemas to package your transformation logic into reusable components across multiple parent flows.

    The expression language has rough edges, but once you internalize the function signatures and the Compose chain discipline, you'll find that most data transformation problems that once seemed to require custom code can be solved entirely within the flow editor. That's a meaningful capability to have.

    Learning Path: Flow Automation Basics

    Previous

    Power Automate Flow Types Explained: Automated, Instant, and Scheduled Flows

    Next

    Securing Power Automate Flows in Production: Managing Credentials, Connection References, and Data Loss Prevention Policies

    Related Articles

    Power Automate🌱 Foundation

    Using Power Automate with Microsoft Teams: Automate Notifications, Approvals, and Channel Messages

    16 min
    Power Automate🔥 Expert

    Implementing Event-Driven Automation with Power Automate and Azure Service Bus: Queue-Based Decoupling for High-Volume, Resilient Enterprise Workflows

    32 min
    Power Automate⚡ Practitioner

    Integrating Power Automate with Microsoft Dataverse: Querying, Creating, and Updating Records in Business Process Flows

    21 min

    On this page

    • Introduction
    • Prerequisites
    • The Expression Editor: A Brief Orientation Before We Go Deep
    • String Functions: Cleaning and Reshaping Text Data
    • The Customer Name Normalization Problem
    • `toLower`, `toUpper`, and the Title Case Gap
    • Extracting Name Parts with `indexOf` and `substring`
    • Email Cleaning with `trim` and `toLower`
    • Using `substring` and `indexOf` for Targeted Extraction
    • `startsWith`, `endsWith`, and `contains` for Conditional Logic
    • Date Functions: Time Is More Complicated Than It Looks
    • The Date Format Chaos Problem
    • `utcNow()` and `formatDateTime()`
    • `addDays`, `addHours`, `addMinutes`, `addSeconds`
    • `ticks()` and Date Arithmetic
    • Parsing Incoming Date Strings
    • Array Functions: Working with Collections of Data
    • Why Arrays Are the Heart of Real Flows
    • `length()`, `first()`, `last()`, and Direct Indexing
    • `filter()`: The Most Powerful Array Function
    • `select()`: Reshaping Array Items
    • `join()`: Turning Arrays into Strings
    • `union()`, `intersection()`, and Deduplication
    • `contains()` with Arrays
    • Chaining It All Together: The Compose Action Pattern
    • Hands-On Exercise: Building a Customer Record Normalizer
    • Common Mistakes and Troubleshooting
    • "InvalidTemplate" Error
    • "The template language expression is not valid" with a long path
    • `filter()` Returns an Empty Array When You Expected Results
    • `ticks()` Fails on Date-Only Strings
    • `select()` Inside Apply to Each Causes Confusion
    • Numbers That Look Like Numbers but Aren't
    • Performance Implications: When Not to Use Expression-Based Transforms
    • Summary and Next Steps