Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power Automate

Automating Recurring Microsoft 365 Reports with Power Automate: Querying SharePoint Lists, Formatting HTML Tables in Outlook Emails, and Scheduling Delivery to Distribution Groups

Learn how to build a production-quality recurring report flow that queries SharePoint lists with OData filters, transforms results into polished HTML tables using Select and join(), and delivers them to distribution groups on a schedule — no manual exporting, no formatting, no missed sends. This lesson covers every design decision from trigger configuration to conditional cell formatting and empty-result handling.

🔥 Expert28 min readOct 14, 2026Updated Oct 14, 2026
Automating Recurring Microsoft 365 Reports with Power Automate: Querying SharePoint Lists, Formatting HTML Tables in Outlook Emails, and Scheduling Delivery to Distribution Groups
On this page
  • Introduction
  • Prerequisites
  • Designing the Flow Architecture Before You Build
  • Stage 1: Configuring the Recurrence Trigger
  • Stage 2: Querying SharePoint with OData Filters
  • Writing the Filter Query
  • Selecting Only the Columns You Need
  • Handling Pagination
  • Stage 3: Transforming List Data into an HTML Table
  • Step 1: Build the Row Template with Select
  • Step 2: Join the Rows into a Table Body String
  • Step 3: Assemble the Full HTML Email Body
  • Handling the Empty Result Set
  • Stage 4: Sending to Distribution Groups via Outlook
  • Configuring the Recipients
  • Configuring the Email Body
  • The Reply-To Anti-Pattern
  • Stage 5: Making the Report Professional — Advanced Table Techniques
  • Alternating Row Colors
  • Sorting the Results
  • Conditional Cell Formatting
  • Stage 6: Chaining Multiple SharePoint Queries in One Report
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • The table shows raw JSON instead of values
  • The HTML in the email shows as literal text with angle brackets
  • Recipients receive the email but images or styles are missing
  • The flow runs but the distribution group doesn't receive the email
  • The flow runs at the wrong time after daylight saving changes
  • `Get items` returns only 100 rows even though more exist
  • Dynamic expressions in the email body are rendering as plain text
  • Production Hardening Considerations
  • Naming and Documentation
  • Error Handling
  • Connection Account Ownership
  • Testing Before First Production Run
  • Data Transformation Patterns Worth Knowing
  • Calculating Totals and Counts in the Email Summary
  • Building Reports from Multiple Time Periods
  • Linking Back to SharePoint Items
  • Summary & Next Steps
  • Automating Recurring Microsoft 365 Reports with Power Automate: Querying SharePoint Lists, Formatting HTML Tables in Outlook Emails, and Scheduling Delivery to Distribution Groups

    Introduction

    Every Monday morning, someone on your team manually exports a SharePoint list to Excel, formats it into a readable table, pastes it into an email, and sends it to a distribution group. The export takes five minutes. The formatting takes fifteen. Hunting down the right DL address takes another three. Multiply that by fifty-two weeks and you've burned through forty hours of human attention on something a scheduled flow could handle permanently — without coffee, without errors, and without ever forgetting to send it.

    This is the core use case we're solving: a fully automated, recurring report pipeline that queries SharePoint list data, transforms it into a polished HTML table, and delivers it to a distribution group via Outlook on a schedule you define. Along the way, we'll cover the subtleties that separate a fragile prototype from something you'd actually trust to run unsupervised — things like filtering SharePoint queries efficiently at the source, building HTML that renders consistently across Outlook clients, handling empty result sets gracefully, and constructing distribution group recipients correctly so delivery never silently fails.

    By the end of this lesson, you'll have built a production-quality scheduled report flow and understand every design decision behind it.

    What you'll learn:

    • How to configure a Recurrence trigger for reliable, time-zone-aware scheduled delivery
    • How to query SharePoint lists efficiently using OData $filter and $select to retrieve only the data you need
    • How to build a dynamic HTML table from SharePoint list results using Select and join() — without looping
    • How to construct Outlook emails with inline HTML that renders correctly in desktop, web, and mobile clients
    • How to target distribution groups as email recipients and avoid the common pitfalls of shared mailbox delivery

    Prerequisites

    You should be comfortable with the Power Automate designer interface and have built at least a few flows before. Familiarity with SharePoint list structure (columns, views, and OData query syntax) is helpful but not required — we'll cover what you need. You'll need:

    • A Power Automate license that includes the SharePoint and Office 365 Outlook connectors (Microsoft 365 Business Basic or higher covers both)
    • A SharePoint list with meaningful columns to report on
    • An Outlook connection authenticated under an account that has Send As or Send On Behalf permission for the sender address you'll use
    • Access to at least one distribution group or Microsoft 365 group email address for testing

    Note

    If you're unclear on which license tier unlocks which connectors, see Understanding Power Automate Licensing: Choosing the Right Plan for Your Automation Needs before proceeding. Running into connector access errors mid-build is a frustrating detour that licensing clarity prevents.


    Designing the Flow Architecture Before You Build

    The most common mistake with report automation flows is starting in the designer immediately and making structural decisions by accident. Before you open Power Automate, spend five minutes answering these questions:

    What data are you reporting on, and what shape should it take? Our example will use a SharePoint list called Project Tracker with columns: Title (project name), Status (choice: Active, On Hold, Completed), Owner (person), DueDate (date), and Budget (currency). We want a weekly Monday-morning report of all Active projects sent to the project-leads@contoso.com distribution group.

    What filtering belongs at the query layer vs. the flow layer? This matters for performance. Filtering inside Power Automate — using Condition actions and loops — means you first retrieve every item from SharePoint and then throw most of them away. Filtering at the OData layer means SharePoint returns only the rows you need. For a list with hundreds of items this is a meaningful difference; for lists with thousands of items it's the difference between a flow that completes in seconds and one that times out.

    How should the email look? Distribution groups typically reach mixed audiences — people reading in Outlook desktop, Outlook Web App, and mobile clients. We want a table that renders without custom fonts or external stylesheets, using only inline CSS, because many email clients strip <head> style blocks.

    With those decisions made, your flow has four logical stages:

    1. Trigger — Recurrence on a defined schedule
    2. Query — SharePoint Get Items with OData filtering
    3. Transform — Convert raw list data into an HTML table string
    4. Deliver — Send an Outlook email to the distribution group

    Let's build each one properly.


    Stage 1: Configuring the Recurrence Trigger

    A Scheduled flow starts with the Recurrence trigger. This seems simple but has several configuration decisions worth understanding deeply. If you haven't worked through the nuances of scheduled triggers before, Scheduling and Managing Time-Based Flows in Power Automate: Recurrence Triggers, Time Zones, and Business Hours Logic is the complete reference — read it alongside this lesson.

    Create a new Scheduled flow and set the following in the Recurrence trigger:

    • Interval: 1
    • Frequency: Week
    • Time zone: Select your organization's primary time zone explicitly — do not leave this as UTC unless your stakeholders genuinely operate in UTC. A report that arrives at 8:00 AM Eastern should be configured as 8:00 AM Eastern, not 13:00 UTC (which breaks when daylight saving shifts the offset).
    • On these days: Monday
    • At these hours: 7 (for 7:00 AM)
    • At these minutes: 0

    Expand the "Add new parameter" dropdown in the Recurrence trigger to reveal the time-zone, days, hours, and minutes fields — they're collapsed by default. Many builders miss these and end up with flows that run daily or at the wrong time because they only set interval and frequency.

    Warning

    The Recurrence trigger does not self-correct for missed runs. If your flow is turned off during a scheduled window and then re-enabled, it will not run retroactively — it will simply wait for the next scheduled occurrence. For a weekly report, this means a missed Monday means stakeholders wait until the following Monday. Build in a manual trigger alternative (an instant flow with the same logic, or a button flow) if the report is high-stakes.

    The trigger produces no output you'll use directly — its job is simply to fire the flow at the right time. Everything downstream depends on what you query.


    Stage 2: Querying SharePoint with OData Filters

    Add a SharePoint — Get items action. Configure:

    • Site Address: Your SharePoint site URL
    • List Name: Project Tracker

    Now expand Show advanced options. This is where the meaningful configuration happens.

    Writing the Filter Query

    The Filter Query field accepts OData query syntax. For our scenario — Active projects only — the filter is:

    Status eq 'Active'
    

    If you wanted to add a date-based filter — say, projects with a DueDate in the next 30 days from when the flow runs — you'd combine conditions:

    Status eq 'Active' and DueDate ge '@{utcNow()}' and DueDate le '@{addDays(utcNow(), 30)}'
    

    Note the single quotes around string values and the use of dynamic expressions inside the OData string. SharePoint's OData implementation has quirks:

    • Choice columns (like Status) filter against their string display values, not numeric IDs
    • Person/People columns filter against Title or Email using a navigation property: Owner/Title eq 'Jane Smith'
    • Date columns expect ISO 8601 format: 2024-01-15T00:00:00Z
    • Lookup columns require the navigation syntax: ProjectCategory/Id eq 3

    The utcNow() expression in a Filter Query works because Power Automate evaluates the expression before sending the query to SharePoint. You can verify the evaluated value in run history.

    Tip

    If your Filter Query returns a 400 error during testing, the most likely culprits are: a column internal name that differs from the display name (check by looking at the list column settings URL), a choice value that doesn't exactly match the stored value including capitalization, or a date format mismatch. The SharePoint Get items action error messages are not always specific — run a version without the filter first to confirm connectivity, then add filters incrementally.

    Selecting Only the Columns You Need

    The Select Columns field (also in advanced options) accepts a comma-separated list of internal column names:

    Title,Status,Owner/Title,DueDate,Budget
    

    Without this, Get items returns every column in the list including internal SharePoint metadata columns — Modified, Created, Editor, _UIVersionString, and dozens more. That extra data increases payload size and can push you toward the 100MB flow action output limit on large lists.

    For person columns, use Owner/Title or Owner/EMail in Select. You also need to add the corresponding Expand parameter: Owner — this tells SharePoint to include the related user record in the response. Without Expand, selecting Owner/Title returns nothing for person columns.

    Handling Pagination

    By default, Get items returns a maximum of 100 items. For our project tracker with potentially hundreds of rows, configure Top Count or enable Pagination in the action settings (the three-dot menu on the action card, then Settings). Pagination allows the action to automatically make multiple requests and concatenate the results, up to a configurable threshold (default 5,000, max varies by connector version).

    For a report flow, enable pagination and set the threshold to comfortably above your expected maximum result count. This replaces the common anti-pattern of using a Do Until loop to manually page through results — a pattern that's slower, harder to read, and easier to get wrong.

    Key insight

    The SharePoint Get items action with pagination enabled effectively hides the complexity of REST API paging from you. The action output — value — contains all items as a single array after pagination completes. This makes the downstream transformation stage much cleaner because you're working with one array, not managing page cursors yourself.


    Stage 3: Transforming List Data into an HTML Table

    This is where most builders get stuck. The naive approach is to add a loop — Apply to each — and build the HTML table row by row, appending to a string variable with each iteration. This works, but it's slow (each loop iteration is a separate action execution), hard to read, and produces HTML that's impossible to inspect without running the flow.

    The professional approach uses two actions: Select to project each item into an HTML row string, and join() to concatenate those rows into the table body — no loop required.

    Step 1: Build the Row Template with Select

    Add a Data Operations — Select action:

    • From: The value output from Get items (this is your array of SharePoint items)
    • Map: Switch to text mode using the toggle in the top-right corner of the Map section

    In text mode, the Map field accepts a single expression that defines what each output element looks like. We want each array element to become an HTML table row. Enter this expression:

    concat(
      '<tr>',
      '<td style="padding:8px;border:1px solid #ddd;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">',
      item()?['Title'],
      '</td>',
      '<td style="padding:8px;border:1px solid #ddd;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">',
      item()?['Status']?['Value'],
      '</td>',
      '<td style="padding:8px;border:1px solid #ddd;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">',
      item()?['Owner']?['Title'],
      '</td>',
      '<td style="padding:8px;border:1px solid #ddd;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">',
      formatDateTime(item()?['DueDate'], 'MMM d, yyyy'),
      '</td>',
      '<td style="padding:8px;border:1px solid #ddd;font-family:Segoe UI,Arial,sans-serif;font-size:13px;text-align:right;">',
      '$',
      string(item()?['Budget']),
      '</td>',
      '</tr>'
    )
    

    A few things to notice:

    item()?['Status']?['Value'] — Choice columns in SharePoint return an object, not a raw string. The Value property holds the display string. If you use just item()?['Status'], you'll get {"Value":"Active","@odata.type":"#Microsoft.Azure.Connectors.SharePoint.SPListExpandedReference"} in your email, which is embarrassing.

    item()?['Owner']?['Title'] — Person columns also return objects. Title gives you the display name. EMail gives you the email address. You accessed this via the Expand parameter earlier; without it, this would be null.

    formatDateTime(item()?['DueDate'], 'MMM d, yyyy') — Raw date values from SharePoint look like 2024-03-15T00:00:00Z. The formatDateTime function converts this to Mar 15, 2024. We cover date formatting in depth in Mastering Dynamic Expressions and the Power Automate Formula Language: String, Date, and Array Functions for Real-World Data Manipulation — worth studying if date handling is giving you trouble.

    Inline CSS on every <td> — Email clients including Outlook desktop notoriously strip <style> blocks and class-based CSS. Every style must be on the element itself. Yes, it's verbose. Yes, it's necessary. Embrace it.

    The output of the Select action is an array of HTML row strings — one per SharePoint item.

    Step 2: Join the Rows into a Table Body String

    Add a Data Operations — Compose action:

    join(outputs('Select')?['body'], '')
    

    This concatenates all the row strings with an empty separator, producing the complete <tbody> content. The outputs('Select')?['body'] reference accesses the Select action's output array. Name this Compose action something descriptive like Compose_TableRows.

    Step 3: Assemble the Full HTML Email Body

    Add another Compose action (name it Compose_EmailBody) and build the complete HTML:

    <div style="font-family:Segoe UI,Arial,sans-serif;font-size:14px;color:#333;">
      <p>Hello,</p>
      <p>Here is your weekly Active Projects summary as of <strong>@{formatDateTime(utcNow(), 'dddd, MMMM d, yyyy')}</strong>.</p>
      <table style="border-collapse:collapse;width:100%;max-width:800px;">
        <thead>
          <tr style="background-color:#0078d4;color:#ffffff;">
            <th style="padding:10px 8px;text-align:left;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">Project</th>
            <th style="padding:10px 8px;text-align:left;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">Status</th>
            <th style="padding:10px 8px;text-align:left;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">Owner</th>
            <th style="padding:10px 8px;text-align:left;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">Due Date</th>
            <th style="padding:10px 8px;text-align:right;font-family:Segoe UI,Arial,sans-serif;font-size:13px;">Budget</th>
          </tr>
        </thead>
        <tbody>
          @{outputs('Compose_TableRows')}
        </tbody>
      </table>
      <p style="margin-top:16px;font-size:12px;color:#888;">This report is generated automatically every Monday at 7:00 AM. Do not reply to this email.</p>
    </div>
    

    The @{outputs('Compose_TableRows')} expression injects the joined row string into the table body. The formatDateTime(utcNow(), 'dddd, MMMM d, yyyy') in the paragraph renders as "Monday, March 18, 2024" — a human-readable date that helps recipients know the report is current.

    Warning

    Power Automate's expression engine will HTML-encode certain characters if you embed dynamic content inside the Compose body as expressions. Specifically, angle brackets from your HTML row strings can get converted to &lt; and &gt;, which renders literally in the email instead of as HTML tags. The Compose action with join() avoids this because you're building the HTML as a string first, then embedding it as a single expression — the encoder sees one string, not raw HTML tags. If you see encoded entities in your test emails, this is the culprit.

    Handling the Empty Result Set

    What happens when Get items returns zero items — no Active projects this week? Your table renders with a header row and an empty body, which looks broken and confuses recipients.

    Add a Condition action after Get items that checks:

    length(outputs('Get_items')?['body/value'])
    

    is greater than 0.

    In the Yes branch: proceed with Select, Compose, and Send Email.

    In the No branch: add a Send Email action with a different body: "No Active projects found for this week. The report will resume automatically when active projects exist."

    This pattern — checking for empty results before building a table — is covered in detail in Working with Conditions, Loops, and Variables in Power Automate, which also covers the broader patterns for branching logic.


    Stage 4: Sending to Distribution Groups via Outlook

    Add a Office 365 Outlook — Send an email (V2) action. This is the version you want — V2 supports HTML bodies natively and has more configuration options than V1.

    Configuring the Recipients

    In the To field, you can enter the distribution group's email address directly: project-leads@contoso.com. Power Automate will resolve this correctly and Outlook will expand it at delivery time.

    Some things that trip people up with distribution groups:

    Multiple groups or mixed recipients: The To field accepts a semicolon-separated list of email addresses: project-leads@contoso.com;cfo@contoso.com. You can also use Cc and Bcc for secondary recipients.

    Dynamic recipient lists from SharePoint: If your flow needs to send to a list of recipients stored in a SharePoint list column (rather than a hardcoded DL), you need to extract those email addresses and join them with semicolons. This is a join() expression over a Select of the email field — the same pattern we used for HTML rows.

    Microsoft 365 Groups vs. Distribution Lists: These behave slightly differently. Microsoft 365 Groups (the modern kind with a shared mailbox, calendar, and SharePoint site) have an email address that can receive external mail if configured for it. Classic distribution lists always expand to individual mailboxes. The Power Automate Send Email action handles both the same way — it just needs a valid SMTP address.

    Tip

    If your distribution group doesn't receive the email during testing but you see the flow run as successful, the problem is almost always permissions. The account whose Outlook connection you're using must be permitted to send to the group — some organizations restrict who can send to distribution lists via Exchange transport rules. Check with your Exchange admin and confirm the DL allows messages from your sender address.

    Configuring the Email Body

    In the Body field of Send an email:

    • Toggle Is HTML to Yes — this is a toggle switch in the action body that's easy to miss
    • In the Body field, enter the expression: outputs('Compose_EmailBody')

    Do not paste your HTML directly into the Body field as static text. If you do, expressions inside it won't evaluate. The Compose action pattern — build the HTML in Compose, reference it in Send Email — keeps your flow readable and ensures expressions resolve correctly.

    Configure the remaining fields:

    • Subject: Weekly Active Projects Report — @{formatDateTime(utcNow(), 'MMMM d, yyyy')}
    • From (Send As): If the flow should appear to come from a shared mailbox (like reports@contoso.com) rather than your personal account, specify the address here. Your account needs Send As permission on that mailbox in Exchange, configured by an admin.
    • Importance: Normal for routine reports; save High for exception-based alerts

    The Reply-To Anti-Pattern

    Distribution group reports often use a shared mailbox as the sender. Stakeholders frequently reply to these emails, which sends replies to the shared mailbox — often unmonitored. Add a footer in your HTML body explicitly telling recipients not to reply, and consider setting a custom Reply-To address (available in the advanced options of Send an email V2) that routes to an actual person or team inbox.


    Stage 5: Making the Report Professional — Advanced Table Techniques

    The basic HTML table works. Let's make it genuinely good.

    Alternating Row Colors

    The Select action builds every row identically. To add alternating row colors (zebra striping) in email HTML, you need to know each row's index. Modify the Select Map expression to use indexOf() to check position:

    concat(
      '<tr style="background-color:',
      if(equals(mod(indexOf(triggerBody()?['rows'], item()), 2), 0), '#f9f9f9', '#ffffff'),
      ';">',
      ...
    )
    

    This is actually complicated in Power Automate because indexOf on arrays compares by reference, not position — and SharePoint items are complex objects. The practical alternative is to set a row index counter using a variable inside an Apply to Each loop if you need alternating colors. For most professional contexts, a clean white table with strong header colors reads better than zebra striping anyway, so this is often premature optimization.

    Sorting the Results

    The Get items action supports an Order By field that accepts OData orderby syntax:

    DueDate asc
    

    or for multi-column sorting:

    Status asc, DueDate asc
    

    Sorting at the query level is more efficient than sorting the array in Power Automate. The sortBy() and sortByDescending() functions exist in Power Automate but are more complex to apply to nested object arrays. Use OData sorting wherever possible.

    Conditional Cell Formatting

    What if you want overdue projects to display their due date in red? You need the if() function inside the row template:

    concat(
      '<td style="padding:8px;border:1px solid #ddd;font-family:Segoe UI,Arial,sans-serif;font-size:13px;color:',
      if(less(item()?['DueDate'], utcNow()), '#cc0000', '#333333'),
      ';">',
      formatDateTime(item()?['DueDate'], 'MMM d, yyyy'),
      '</td>'
    )
    

    less(item()?['DueDate'], utcNow()) compares the ISO date string to the current UTC time. Since both are ISO 8601 strings, the lexicographic comparison actually works correctly for full datetime values. If your DueDate is a date-only field (no time component), add T00:00:00Z to normalize:

    less(concat(item()?['DueDate'], 'T00:00:00Z'), utcNow())
    

    This kind of conditional formatting elevates a routine report into something genuinely useful — recipients can see at a glance which projects are overdue without having to interpret dates manually.


    Stage 6: Chaining Multiple SharePoint Queries in One Report

    A single report often needs to surface data from multiple lists. For example: Active Projects (from Project Tracker) plus pending action items (from Action Items list). Rather than sending two separate emails, you can query both lists, build two separate HTML tables, and include both in a single email body.

    The pattern:

    1. Recurrence trigger
    2. Get items — Project Tracker (filtered: Status eq 'Active')
    3. Get items — Action Items (filtered: AssignedTo/Title eq 'Team Lead' and Completed eq 0)
    4. Select — build Project rows
    5. Compose — join Project rows
    6. Select — build Action Item rows
    7. Compose — join Action Item rows
    8. Compose — assemble full email with both tables, section headers, and dividers
    9. Send email

    This is a linear chain — each query runs sequentially by default. If the two queries are independent (which they usually are), you can save execution time by running them in parallel. In the designer, you achieve this by adding both Get items actions directly after the trigger rather than chaining them — Power Automate detects that they have no dependency on each other and executes them in parallel.

    For deeper patterns on chaining multiple Microsoft 365 connectors in a single flow, see Chaining Microsoft 365 Connectors in a Single Flow: Building Multi-Step Automations That Span Outlook, Teams, SharePoint, and Planner Without Losing Data Between Actions.

    Key insight

    The Select + join() pattern for building HTML from arrays is infinitely composable. Build each table independently as a Compose output, then assemble them into a final Compose that structures the full email. This keeps each transformation unit testable in isolation — you can inspect each Compose output in run history to see exactly what HTML is being generated at each stage.


    Hands-On Exercise

    Build the following flow from scratch, which is slightly more complex than the walkthrough above:

    Scenario: Your company tracks vendor contracts in a SharePoint list called Vendor Contracts with columns: VendorName (text), ContractValue (number), ExpiryDate (date), ContractManager (person), and RenewalStatus (choice: Pending Review, In Negotiation, Renewed, Expired).

    Build a flow that:

    1. Runs every Friday at 3:00 PM in the America/Chicago time zone
    2. Queries all contracts where RenewalStatus is "Pending Review" or "In Negotiation"
    3. Builds an HTML table sorted by ExpiryDate ascending (soonest expiring first)
    4. Colors the ExpiryDate cell red if the expiry date is within 30 days of the current date (addDays(utcNow(), 30))
    5. Includes a summary line above the table showing the total contract value at risk (sum of ContractValue for all returned rows)
    6. Handles the empty case with a friendly "No contracts require immediate attention" message
    7. Sends to legal-team@yourcompany.com from compliance-reports@yourcompany.com

    Hints for the challenging parts:

    For filtering with OR in OData: use (RenewalStatus eq 'Pending Review' or RenewalStatus eq 'In Negotiation'). Note the parentheses — some SharePoint OData implementations require them for OR conditions.

    For the total contract value: after Get items, add a Select action that projects each item to just its ContractValue number, then use sum(outputs('Select_ContractValues')?['body']) in a Compose action. Reference that Compose in your email body.

    For the 30-day date threshold in cell coloring: less(item()?['ExpiryDate'], addDays(utcNow(), 30)) — compare ExpiryDate against now-plus-30-days rather than just now.

    For the Send As configuration: you'll need to verify your account has Send As permissions on compliance-reports@yourcompany.com in Exchange Admin Center before the flow will succeed. If it fails with a 403 on the Send Email action, this is the first thing to check.


    Common Mistakes & Troubleshooting

    The table shows raw JSON instead of values

    You forgot the ?['Value'] suffix on a Choice column, or you forgot the ?['Title'] suffix on a Person column. SharePoint complex field types return objects, not primitives. Inspect the raw output of your Get items action in run history — click the action, look at the Outputs section — to see the actual JSON structure of your data and identify what property path you need.

    The HTML in the email shows as literal text with angle brackets

    You used a variable to build your HTML incrementally inside an Apply to Each loop, and the Append to string variable action encoded your angle brackets. This is a known behavior when building HTML with string variables in loops. The fix is the Select + join() pattern described in Stage 3 — it avoids the encoding issue entirely.

    Recipients receive the email but images or styles are missing

    You used an external stylesheet link or referenced external images. Email clients block external resources by default for security. All styling must be inline, and any images must be embedded as Base64 data URIs (complex) or hosted at a publicly accessible URL with a known-stable domain. For professional report emails, skip images entirely and rely on well-structured HTML with color and typography.

    The flow runs but the distribution group doesn't receive the email

    Check these in order: (1) Does the DL allow external senders, or only internal? (2) Does the sender account have permission to send to the DL? (3) Is there an Exchange transport rule blocking automated emails? (4) Did the email go to the DL's spam folder? Run a test by sending to a personal email address first to confirm the Send Email action itself is working, then isolate the DL configuration issue.

    The flow runs at the wrong time after daylight saving changes

    You configured the Recurrence trigger in UTC rather than a named time zone. UTC doesn't observe daylight saving — it stays constant while local time shifts. If your organization operates in America/Eastern and you configured the flow in UTC, it will shift by an hour twice a year. Always set the time zone explicitly using the named time zone parameter.

    `Get items` returns only 100 rows even though more exist

    Pagination is not enabled. Open the action's Settings panel (the three-dot menu on the action card header), enable Pagination, and set the threshold. Note that enabling pagination can significantly increase flow run time for large lists because Power Automate is making multiple sequential API calls behind the scenes. If you have thousands of items, consider whether the OData filter can reduce the result set to a manageable size before enabling pagination with a very high threshold.

    Warning

    The Get items action with pagination enabled and no filter on a list with tens of thousands of items can run for several minutes and approach flow timeout limits. Always filter at the OData layer first, then use pagination as a safety net for results that legitimately number in the hundreds.

    Dynamic expressions in the email body are rendering as plain text

    You forgot to toggle Is HTML to Yes in the Send an email action. When Is HTML is off, the body is treated as plain text and HTML tags render literally. Toggle it on and the browser rendering engine in the email client will interpret the markup.

    For more advanced debugging using run history inspection, the Using Power Automate Run History and Flow Checker to Debug and Fix Failing Flows lesson walks through exactly how to trace execution failures and inspect action inputs and outputs.


    Production Hardening Considerations

    Before you deploy this flow to run permanently, address these concerns:

    Naming and Documentation

    Name your flow descriptively: SCHED_WeeklyActiveProjects_ReportEmail. Name each action in the flow with a descriptive label — Get Active Projects, Build HTML Rows, Join Rows to String, Compose Email Body, Send to Project Leads DL. When you're debugging a flow that has run 200 times, meaningful action names in run history are invaluable. See Power Automate Best Practices: Master Flow Naming, Testing, and Monitoring for a naming convention framework.

    Error Handling

    What happens if SharePoint is temporarily unavailable during a scheduled run? Without error handling, the flow fails silently and no email is sent. Add a Configure run after setting on your Send Email action to also run if the preceding actions fail, and send an alert email to a technical contact indicating that the report failed to generate. For a full treatment of error handling patterns, Master Error Handling and Retry Patterns in Power Automate for Bulletproof Flows is the definitive reference.

    Connection Account Ownership

    The flow runs under the connections of the account that created it. If that person leaves the organization or their account is disabled, the flow breaks immediately. In production, use service account connections or shared connections where possible, and document which account owns each connection. This is an organizational governance concern beyond the scope of individual flow design but critical to long-term flow health.

    Testing Before First Production Run

    Turn on the flow, then manually trigger it using the Test function (Run button → Test → With a manually triggered run, then trigger via the Recurrence trigger's test option). Verify: the email arrives at the DL, the table renders correctly in Outlook desktop and web, dates and currency format correctly, and the empty-state path works (temporarily change your OData filter to return no results and confirm the fallback email sends).


    Data Transformation Patterns Worth Knowing

    Once you have the basic pipeline working, a few additional transformation patterns come up repeatedly in reporting flows.

    Calculating Totals and Counts in the Email Summary

    A report that shows a table is useful. A report that shows a table and a summary line ("12 active projects | Total budget: $2.4M") is excellent. After your Select action that builds rows, add a second Select that projects each item to just the numeric field, then use sum() and length():

    Total projects: @{length(outputs('Get_items')?['body/value'])}
    Total budget: $@{formatNumber(sum(outputs('Select_Budgets')?['body']), '0,0')}
    

    The formatNumber() function isn't native to Power Automate — you'd use a combination of string() and some creative string manipulation, or accept that the number renders without formatting. For currency formatting, the cleanest approach is to format inside the Select Map expression where each row's budget is already being processed.

    Building Reports from Multiple Time Periods

    Weekly reports often need to show "this week's data" vs. "last week's data." You need date range filters anchored to the report run date:

    // Start of current week (Monday)
    addDays(startOfDay(utcNow()), mul(sub(dayOfWeek(utcNow()), 1), -1))
    
    // This expression calculates the previous Monday as the week boundary
    

    Date math in Power Automate expressions requires understanding startOfDay(), dayOfWeek(), addDays(), and utcNow() in combination. It's one of the more complex areas of the expression language, and it's worth spending time in Mastering Dynamic Expressions and the Power Automate Formula Language: String, Date, and Array Functions for Real-World Data Manipulation to get fluent with it.

    Linking Back to SharePoint Items

    Every row in your table can link to the source SharePoint item, giving recipients a one-click path to the actual record. In the Select Map, build anchor tags:

    concat(
      '<td style="padding:8px;border:1px solid #ddd;"><a href="',
      'https://contoso.sharepoint.com/sites/Projects/Lists/Project%20Tracker/DispForm.aspx?ID=',
      item()?['ID'],
      '" style="color:#0078d4;text-decoration:none;">',
      item()?['Title'],
      '</a></td>'
    )
    

    The item()?['ID'] gives you the SharePoint list item ID, which you append to the display form URL. This transforms a static report into an interactive navigation tool — recipients see the summary in email and can click through to the full item in SharePoint without searching.

    For more patterns on integrating SharePoint list data with Power Automate, including read, write, and lifecycle management scenarios, see Automating SharePoint List Item Lifecycle Management with Power Automate: Creating, Updating, Archiving, and Deleting Records Based on Business Rules.


    Summary & Next Steps

    You've built a production-quality recurring report pipeline that:

    • Triggers reliably on a schedule with correct time-zone handling
    • Queries SharePoint with OData filters at the source — not in the flow — for performance efficiency
    • Transforms raw list data into formatted HTML tables using Select and join() instead of slow, fragile loops
    • Handles edge cases including empty result sets, choice column objects, person column objects, and overdue date formatting
    • Delivers polished HTML email to distribution groups with correct Is HTML configuration and conditional cell formatting
    • Accounts for the empty-result case with a graceful fallback message

    The pattern you've learned — trigger, query, transform with Select + join, deliver — generalizes to virtually any recurring data report you'd need to build in the Microsoft 365 ecosystem. Change the SharePoint list, change the column names in the Select Map, change the schedule and recipient, and you have a new report in under ten minutes.

    Where to go from here:

    If your reports need to include data from non-SharePoint sources — REST APIs, Dataverse tables, or Azure SQL — Connecting Power Automate to External APIs and Services covers the HTTP connector patterns you'd use to extend the query layer. If your organization wants to enhance these reports with AI-generated summaries or anomaly callouts, Building and Deploying Power Automate Flows That Call Azure OpenAI and AI Builder Models shows how to add an AI Builder step between your data retrieval and your email composition that generates a plain-language summary of the data before the table.

    The recurring report flow you've built today is also a foundation for more sophisticated patterns — exception-based alerting (send the email only when certain thresholds are breached), triggered reports (send on demand when a SharePoint item reaches a particular status), and multi-audience delivery (personalized report versions sent to different recipients based on data segmentation). Each of those builds directly on the patterns you've internalized here.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Flow Automation Basics

    Previous

    Automating SharePoint-Based Leave Requests with Power Automate: Building an End-to-End Submission, Approval, and Calendar Event Flow Using Microsoft 365 Connectors

    Related Insights

    Power AutomatePractitioner

    Implementing Power Automate Flow Run Concurrency Limits and Queue Depth Controls to Prevent Connector Overload in High-Throughput Enterprise Pipelines

    25 min
    Power AutomateFoundation

    Scheduling and Running Desktop Flows from Cloud Flows on a Timer: Configuring Recurrence Triggers, Passing Runtime Parameters, and Logging Results to SharePoint

    17 min
    Power AutomatePractitioner

    Automating SharePoint-Based Leave Requests with Power Automate: Building an End-to-End Submission, Approval, and Calendar Event Flow Using Microsoft 365 Connectors

    21 min

    On this page

    • Introduction
    • Prerequisites
    • Designing the Flow Architecture Before You Build
    • Stage 1: Configuring the Recurrence Trigger
    • Stage 2: Querying SharePoint with OData Filters
    • Writing the Filter Query
    • Selecting Only the Columns You Need
    • Handling Pagination
    • Stage 3: Transforming List Data into an HTML Table
    • Step 1: Build the Row Template with Select
    • Step 2: Join the Rows into a Table Body String
    • Step 3: Assemble the Full HTML Email Body
    • Handling the Empty Result Set
    • Stage 4: Sending to Distribution Groups via Outlook
    • Configuring the Recipients
    • Configuring the Email Body
    • The Reply-To Anti-Pattern
    • Stage 5: Making the Report Professional — Advanced Table Techniques
    • Alternating Row Colors
    • Sorting the Results
    • Conditional Cell Formatting
    • Stage 6: Chaining Multiple SharePoint Queries in One Report
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • The table shows raw JSON instead of values
    • The HTML in the email shows as literal text with angle brackets
    • Recipients receive the email but images or styles are missing
    • The flow runs but the distribution group doesn't receive the email
    • The flow runs at the wrong time after daylight saving changes
    • `Get items` returns only 100 rows even though more exist
    • Dynamic expressions in the email body are rendering as plain text
    • Production Hardening Considerations
    • Naming and Documentation
    • Error Handling
    • Connection Account Ownership
    • Testing Before First Production Run
    • Data Transformation Patterns Worth Knowing
    • Calculating Totals and Counts in the Email Summary
    • Building Reports from Multiple Time Periods
    • Linking Back to SharePoint Items
    • Summary & Next Steps