Building a Clean Microsoft Teams Notification Card from a SharePoint List with Power Automate
When a SharePoint list item is created or updated, a plain Teams message works, but it rarely looks professional. The message becomes a block of text, long URLs break the layout, and multiline fields lose visual structure.
A better pattern is to post an Adaptive Card from Power Automate. Microsoft documents Adaptive Cards in Power Automate as the recommended way to share structured information in Teams without relying on custom HTML or CSS. Adaptive Cards are authored in JSON and rendered as native UI inside the host application. (Microsoft Learn)
In this article, I will refactor the notification design using generic field names only, so the example is safe for documentation, reusable, and ready to adapt to any SharePoint list.
Why not use raw HTML or a classic message body?
This is the first architectural decision to get right.
In Teams, Adaptive Cards are the right option for structured workflow notifications. Microsoft states that Workflows support Adaptive Cards only and do not support the older MessageCard model used by legacy Office 365 connectors. Microsoft also notes that HTML is not supported in Adaptive Cards, while Markdown is supported in certain text fields such as TextBlock, Fact.Title, and Fact.Value. (Microsoft Learn)
That means this approach is not ideal:
<table> <tr> <td>Title</td> <td>My task</td> </tr></table>
Even if HTML appears in a Compose action, it does not mean the downstream Teams action will render it as a polished card. For a modern Teams notification, JSON-based Adaptive Cards are the better design. (Microsoft Learn)
The target architecture
The simplest production-friendly pattern is this:
SharePoint trigger
→ optional Compose actions for each field
→ Microsoft Teams: Post adaptive card in a chat or channel
This pattern keeps the flow readable and makes debugging much easier. Microsoft’s Power Automate documentation also recommends keeping cards simple and using simple blocks of data instead of overly complex table arrays. (Microsoft Learn)
Renaming all field names for a reusable article
To keep the article generic and reusable, I am replacing the original column names with neutral placeholders.
Original concept to generic article mapping
| Original idea | Generic article label | Generic internal token example |
|---|---|---|
| Title | Request Title | RequestTitle |
| Description | Request Details | RequestDetails |
| SharePoint Team Observations | Review Notes | ReviewNotes |
| Modified | Last Updated | LastUpdated |
| Link to item | Open Item Link | ItemLink |
This does not mean you must rename your real SharePoint columns in production. It simply means that, in the article and code sample, I am using clean placeholder names so the design can be understood without exposing project-specific schema.
Recommended card layout
For a task or request notification, the visual order should follow the way people read status updates:
- notification title
- main request title
- details
- review or support notes
- last updated timestamp
- button to open the list item
That sequence gives the message a business-friendly flow and works well in Teams chat.
The Adaptive Card design
Microsoft recommends Adaptive Cards in Teams for structured UI, and Teams design guidance also points to ColumnSet when you want a table-like arrangement or grid layout. For this scenario, however, a FactSet is simpler and works very well for label-value display. (Microsoft Learn)
Here is a clean, anonymized card using generic field names.
{ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.4", "msteams": { "width": "Full" }, "body": [ { "type": "Container", "style": "emphasis", "bleed": true, "items": [ { "type": "TextBlock", "text": "SharePoint Request Notification", "weight": "Bolder", "size": "Large", "wrap": true }, { "type": "TextBlock", "text": "A request was created or updated in the SharePoint list.", "spacing": "Small", "isSubtle": true, "wrap": true } ] }, { "type": "Container", "spacing": "Medium", "items": [ { "type": "FactSet", "facts": [ { "title": "Request Title", "value": "@{triggerOutputs()?['body/RequestTitle']}" }, { "title": "Request Details", "value": "@{triggerOutputs()?['body/RequestDetails']}" }, { "title": "Review Notes", "value": "@{triggerOutputs()?['body/ReviewNotes']}" }, { "title": "Last Updated", "value": "@{triggerOutputs()?['body/LastUpdated']}" } ] } ] } ], "actions": [ { "type": "Action.OpenUrl", "title": "Open Request", "url": "@{triggerOutputs()?['body/ItemLink']}" } ]}
A safer implementation with Compose actions
In real flows, I usually recommend isolating values with Compose actions before assembling the card. This makes troubleshooting easier, especially when a SharePoint internal name is not what you expected.
Use one Compose per value.
Compose_RequestTitle
@{triggerOutputs()?['body/RequestTitle']}
Compose_RequestDetails
@{triggerOutputs()?['body/RequestDetails']}
Compose_ReviewNotes
@{triggerOutputs()?['body/ReviewNotes']}
Compose_LastUpdated
@{triggerOutputs()?['body/LastUpdated']}
Compose_ItemLink
@{triggerOutputs()?['body/ItemLink']}
Then use the Compose outputs inside the card:
{ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.4", "msteams": { "width": "Full" }, "body": [ { "type": "Container", "style": "emphasis", "bleed": true, "items": [ { "type": "TextBlock", "text": "SharePoint Request Notification", "weight": "Bolder", "size": "Large", "wrap": true }, { "type": "TextBlock", "text": "A request was created or updated in the SharePoint list.", "spacing": "Small", "isSubtle": true, "wrap": true } ] }, { "type": "Container", "spacing": "Medium", "items": [ { "type": "FactSet", "facts": [ { "title": "Request Title", "value": "@{outputs('Compose_RequestTitle')}" }, { "title": "Request Details", "value": "@{outputs('Compose_RequestDetails')}" }, { "title": "Review Notes", "value": "@{outputs('Compose_ReviewNotes')}" }, { "title": "Last Updated", "value": "@{outputs('Compose_LastUpdated')}" } ] } ] } ], "actions": [ { "type": "Action.OpenUrl", "title": "Open Request", "url": "@{outputs('Compose_ItemLink')}" } ]}
This version is much easier to maintain.
Why this looks better in Teams
The improvement is not only aesthetic. It is architectural.
With an Adaptive Card:
- the header is visually separated
- labels and values are aligned cleanly
- long URLs are hidden behind a button
- multiline text is easier to read
- the message looks like a workflow notification instead of a copied email fragment
Microsoft describes Adaptive Cards as a host-native way to display blocks of information without the complexity of custom CSS or HTML rendering. That is exactly why they fit this scenario so well. (Microsoft Learn)
Important note about SharePoint internal names
This is one of the most common pitfalls.
Your SharePoint display name might be:
Review Notes
But the internal name could be something else entirely, especially if the field was renamed after creation. For example, spaces may be encoded, or a legacy name may remain behind the scenes.
So in production, always validate the internal name before finalizing expressions. A practical way is:
- insert the field once from Dynamic content
- save and test the flow
- inspect the run history output
- confirm the exact property exposed by the trigger
That is why the Compose-first approach is often the safest option.
Suggested production pattern
For a stable implementation, I recommend this exact flow structure:
| Order | Action | Purpose |
|---|---|---|
| 1 | When an item is created or modified | Detect SharePoint changes |
| 2 | Compose_RequestTitle | Isolate the title |
| 3 | Compose_RequestDetails | Isolate the long text field |
| 4 | Compose_ReviewNotes | Isolate the support or review notes |
| 5 | Compose_LastUpdated | Isolate the timestamp |
| 6 | Compose_ItemLink | Isolate the item URL |
| 7 | Post adaptive card in a chat or channel | Send the final Teams notification |
This structure is simple, readable, and easier to debug than a single large expression block.
Design refinements you can add later
Once the card is working, you can evolve it in a controlled way.
A few good next steps are:
- format the timestamp before sending it
- truncate extremely long text fields
- add an environment label such as Dev, Test, or Production
- include a status field with a stronger visual emphasis
- switch from
FactSettoColumnSetif you want a more custom visual grid
For most SharePoint-to-Teams notifications, though, the FactSet version is already clean and effective.
Final anonymized sample mapping
Below is the final neutral mapping used in this article:
| Functional meaning | Card label | Placeholder field token |
|---|---|---|
| Main request name | Request Title | RequestTitle |
| Main description | Request Details | RequestDetails |
| Team commentary | Review Notes | ReviewNotes |
| Last modification date | Last Updated | LastUpdated |
| SharePoint item URL | Open Request | ItemLink |
Conclusion
If the goal is to send a beautiful and structured notification to Teams from a SharePoint-triggered Power Automate flow, the correct path is to use an Adaptive Card, not a raw HTML table and not a plain message body.
This gives you a cleaner interface, a more professional result, and a much more maintainable flow. It also aligns with Microsoft’s current guidance for Teams workflows, where Adaptive Cards are the supported modern card model. (Microsoft Learn)
Step summary table
| Step | What to do | Recommended implementation |
|---|---|---|
| 1 | Keep the SharePoint trigger | When an item is created or modified |
| 2 | Avoid raw HTML in the Teams message | Do not use <table>, <tr>, or <td> for this scenario |
| 3 | Use a card-based Teams action | Post adaptive card in a chat or channel |
| 4 | Rename fields for documentation | Use placeholders such as RequestTitle, RequestDetails, ReviewNotes |
| 5 | Build the layout in JSON | Adaptive Card with FactSet |
| 6 | Add a clean link experience | Action.OpenUrl button |
| 7 | Improve maintainability | Use Compose actions before the final card |
Technical reference table
| Topic | Key point | Source |
|---|---|---|
| Adaptive Cards in Power Automate | Adaptive Cards are used to share information in Teams using JSON | Microsoft Learn (Microsoft Learn) |
| Creating cards in flows | Keep cards simple and prefer simple blocks of data | Microsoft Learn (Microsoft Learn) |
| HTML support | HTML is not supported in Adaptive Cards in Teams | Microsoft Learn (Microsoft Learn) |
| Markdown support | Markdown is supported in TextBlock, Fact.Title, and Fact.Value | Microsoft Learn (Microsoft Learn) |
| Teams workflow support | Workflows support Adaptive Cards only, not legacy MessageCards | Microsoft Learn (Microsoft Learn) |
| Card design layout | ColumnSet can be used when a table-like layout is needed | Microsoft Learn (Microsoft Learn) |
