Post

Extract an HTML Table from an Email in Power Automate, with XPath, from Damien Bird

How to extract data from an HTML table inside an email body in Power Automate, by parsing the HTML as XML and selecting rows and columns with XPath expressions.

Extract an HTML Table from an Email in Power Automate, with XPath, from Damien Bird

TL;DR: Wrap the email body in xml() and select cells with xpath(): xpath(xml(triggerBody()?['body']), '//table//tr/td[2]/text()') returns the second column as an array of strings. Write a single quote inside the XPath as two single quotes, for example '//table[@class=''data-table'']//tbody/tr'. If xml() fails, the HTML is not well-formed: fix the known culprits with replace() or split the text yourself.

A report or invoice that arrives as an HTML table in an email body is structured data trapped in markup. Damien Bird parses that HTML as XML and pulls the exact values out with XPath, instead of regex or manual text splitting.

💡 Challenge

An HTML table has real structure - rows, cells, headers - that a plain text search throws away. Splitting the raw body on line breaks and delimiters breaks as soon as the sender’s template changes a little. You want “the second cell of every row in this table” or “only rows that contain a specific word”, not a guess.

✅ Solution

Parse the email body as XML with xml() and navigate it with xpath(). Both are standard expressions in Power Automate, no premium connector needed. When the HTML is not well-formed, which happens often in real emails, clean the few known problems with replace() first, or fall back to split().

🔧 How It’s Done

1. Start with a sample in a Compose action.

🔸 Put a sample of the HTML into a Compose action, here named HtmlA. In your flow you later use the email body instead, for example triggerBody()?['body'].

1
2
3
4
5
<html><body><p>Order confirmation</p>
<table class="data-table">
<thead><tr><th>Product</th><th>Qty</th></tr></thead>
<tbody><tr><td>Widget</td><td>5</td></tr><tr><td>Gadget</td><td>7</td></tr></tbody>
</table></body></html>

2. Select what you need with xpath().

These expressions were run in a Compose action against the sample above:

Goal Expression Output
All data rows of a table xpath(xml(outputs('HtmlA')), '//table[@class=''data-table'']//tbody/tr') 2 items, each a <tr> element with its <td> cells
One column as values xpath(xml(outputs('HtmlA')), '//table[@class=''data-table'']//tbody/tr/td[2]/text()') ["5","7"]
Rows containing a word xpath(xml(outputs('HtmlA')), '//tr[td[contains(text(), ''Widget'')]]') 1 item: the Widget row
Table found by its heading, first column xpath(xml(outputs('HtmlA')), '//table[.//th[contains(text(), ''Product'')]]//tr/td[1]/text()') ["Widget","Gadget"]
Number of data rows xpath(xml(outputs('HtmlA')), 'count(//tbody/tr)') 2

Power Automate designer: a Compose action with the expression editor open showing xpath(xml(outputs('HtmlA')), '//table[@class=''data-table'']//tbody/tr/td[2]/text()') The one-column expression in a Compose action. The expression editor shows the doubled single quotes around data-table, and the Inputs field collapses the whole expression to an xpath(...) token.

🔸 Quoting: inside a Power Automate expression a single quote is written as two single quotes (''data-table''). The backslash version (\'data-table\') is rejected when you save the flow: Unable to parse template language expression ... expected token 'RightParenthesis' and actual 'Identifier'.

🔸 Elements vs. text: an XPath ending in /tr returns XML elements. Add /text() on a cell path (for example td[2]/text()) to get plain string values.

3. Handle a namespace on the root element.

🔸 If the root element looks like <html xmlns="http://www.w3.org/1999/xhtml">, xml() still works, but //table//tr/td[2]/text() returns an empty array []. Match by local name instead:

1
xpath(xml(outputs('HtmlC')), '//*[local-name()="table"]//*[local-name()="tr"]/*[local-name()="td"][2]/text()')

🔸 Output for the sample table: ["5","7"].

4. Repair HTML that is not well-formed.

xml() is a strict XML parser, so real-world email HTML often fails. Three failures, with the error of xml():

Problem in the HTML Error message
Unclosed <td> and <tr> tags The provided value cannot be converted to XML: 'The 'td' start tag on line 1 position 110 does not match the end tag of 'table'.
<br> without closing slash The provided value cannot be converted to XML: 'The 'br' start tag on line 1 position 19 does not match the end tag of 'body'.
&nbsp; entity The provided value cannot be converted to XML: 'Reference to undeclared entity 'nbsp'.

Fixes that work:

🔸 Entity: xpath(xml(replace(outputs('HtmlD'), '&nbsp;', ' ')), '//td/text()') returned ["Widget Pro","5"] for <td>Widget&nbsp;Pro</td>.

🔸 Unclosed <br> (rest of the HTML well-formed): xpath(xml(replace(outputs('HtmlE'), '<br>', '<br/>')), '//td/text()') returned ["Widget","5","Gadget","7"].

🔸 Unclosed table cells: skip XML entirely and split the text. Use a Select action with From skip(split(outputs('HtmlB'), '<td>'), 1) and Map first(split(item(), '<')). For <tr><td>Widget<td>5<tr><td>Gadget<td>7 this returned ["Widget","5","Gadget","7"]. This works for cells written exactly as <td>; cells with attributes need a different split string.

5. Switch to the real email body.

🔸 Replace outputs('HtmlA') with the body of your trigger or Get email action, and check the row count with count(//tbody/tr) before you process the rows.

🎉 Result

The flow reads the table cells as a clean array of values, ready for an Apply to each, a SharePoint list, Excel or Dataverse. The Compose test shows the exact output, or the exact xml() error, within seconds.

🌟 Key Advantages

🔸 Structure instead of guessing: rows, cells and headers are addressed directly.

🔸 Standard expressions: xml(), xpath(), replace() and split(), no premium connector.

🔸 Fits many sources: invoice and order emails, recurring HTML reports, any vendor email without an API, as long as the table layout is consistent.

⚠️ Good to Know

🔸 Template changes upstream change which cells a position-based XPath like td[2] returns. Anchor selectors to stable text such as a header name where possible, and check the row count before processing.

🎥 Video Tutorial

🛠️ FAQ

1. How can I extract an HTML table from an email in Power Automate?

Parse the email body with xml() and select rows or cells with xpath(), for example xpath(xml(triggerBody()?['body']), '//table//tr/td[2]/text()'). A single quote inside the XPath is written as two single quotes.

2. Why does my xpath expression fail when I save the flow?

A single quote inside an expression string is escaped by doubling it (''), not with a backslash. The backslash version fails with Unable to parse template language expression.

3. Why does xml() fail on my email body?

xml() needs well-formed XML. Unclosed tags such as <br> or <td> without </td>, and HTML entities such as &nbsp;, fail with The provided value cannot be converted to XML. Replace the entity or the tag with replace(), or split the text with split().

4. Why does xpath() return an empty array although the table is there?

The HTML root element probably declares a namespace (xmlns). Match elements with local-name(), for example //*[local-name()="td"]/text().

5. How do I get plain values instead of XML elements?

End the XPath with /text(), for example //tbody/tr/td[2]/text().

🔸 #PowerPlatformTip 96: ‘Scraping Web Data with Standard Connectors’ - more HTML-parsing techniques for pulling structured data with connectors you already have.

🔸 #PowerPlatformTip 150: ‘JSON & HTML to Markdown with Office Scripts’ - another way to turn messy HTML content into clean, structured output.


You can see this video here on my blog because I have rated this video with 5 stars in my YouTube video library. This video was automatically posted using PowerAutomate.

This tip is by Marcel Lehmann, Microsoft MVP (Business Applications) and founder of the Power Platform Academy – live Power Platform workshops, free or CHF 49. Join the workshop waitlist to get notified about new dates. For companies that want this implemented for them, see KMUpower – Power Platform consulting & automation for SMEs (DACH).

This post is licensed under CC BY 4.0 by the author.

Microsoft and the Power Platform product names (for example Power Apps, Power Automate, Copilot Studio) are trademarks of the Microsoft group of companies. This is an independent website. It is not operated, sponsored or endorsed by Microsoft.