使用Outlook Actionable Messages实现简单的审批功能

我是微软Dynamics 365 & Power Platform方面的工程师/顾问罗勇,也是2015年7月到2018年6月连续三年Dynamics CRM/Business Solutions方面的微软最有价值专家(Microsoft MVP),这是我的第479篇原创文章,写于2022年8月27日。

Power Apps的Out-of-Box审批功能我的B站上有介绍,详见​​五分钟创建一个审批工作流并在邮件中进行审批测试​​​ 。功能很好(也是从本人项目实践应用该功能的精力来讲),还支持移动端,但是偶尔有些特色小需求不是很好实现,比如拒绝的时候要求备注必须输入。这个审批的功能是基于Microsoft Dataverse和​​Outlook Actionable Messages​​ 的,今天我就简单做个例子发送Actionable Message到Outlook中。本文借鉴了​​Actionable Messages step-by-step​​​ 这个视频,如果大家观看不了可以到我的B站去​​观看​​。官方文档如下:

  • ​​Actionable messages in Outlook and Office 365 Groups​​
  • ​​Get started with actionable messages in Office 365​​
  • ​​Send an actionable message via email in Office 365​​
  • ​​Invoke an Outlook add-in from an actionable message​​
  • ​​Refresh an actionable message when the user opens it​​
  • ​​Security requirements for actionable messages in Office 365​​
  • ​​Designing Outlook Actionable Message cards with the Adaptive Card format​​
  • ​​Register your service with the actionable email developer dashboard​​

道理不讲太多,就带大家做个最简单的例子。

首先我们准备用户点击邮件中链接/操作执行要调用的Web API。最简单的就是做个Flow来处理,我们用这种最简单的方式。到​​https://make.powerautomate.com/​​ 中的Solution里面新增一个Clou flow:

为Flow取个名字,记得Trigger一定要选择 When an HTTP request is received。

使用Outlook Actionable Messages实现简单的审批功能

整个Flow如下,很简单,因为我这个是用于演示的,最后一个Response步骤返回的Status Code为200,这样用户点击后会看到成功提示。

使用Outlook Actionable Messages实现简单的审批功能

保存这个Flow,会生成一个URL,Copy这个URL后面的步骤要用。

使用Outlook Actionable Messages实现简单的审批功能

然后要到​​developer dashboard​​ 去注册一个Provider, 点击 New Provider 链接。

使用Outlook Actionable Messages实现简单的审批功能

Friendly Name取个有意义的名字即可,Sender email address from which actionable emails will originate 填入Actional Message发送邮件的邮箱,我这里特意创建了一个notification为用户名的邮箱,Target URLs填入前面步骤创建的Cloud flow的URL,这里可以填写多个,也支持正则表达式匹配多个。需要把自动生成的 Provider Id (originator) 字段的值复制下来备用。

使用Outlook Actionable Messages实现简单的审批功能

至于Scope of submission的话我选第二个Organization,因为我自己就是这个Tenant的Global Admin。然后提交。

使用Outlook Actionable Messages实现简单的审批功能

提交后Exchange Admin会收到邮件提醒去审批,点击View submission浏览并Approve就可以,批准并生效可能需要点时间,不会超过24小时。

使用Outlook Actionable Messages实现简单的审批功能

Actionable Message的内容是Adaptive Card,自己手写太麻烦,有辅助工具,我们这里打开​​Adaptive Card Designer​​ ,使用 Expense Approval来创建一个。

使用Outlook Actionable Messages实现简单的审批功能

因为是演示性质的,所以对于这个Message的内容我也不更改了,直接点击下面的 Edit JSON Payload 链接将JSON内容复制下来。

使用Outlook Actionable Messages实现简单的审批功能

我这里复制下来的内容如下:

{
"type": "AdaptiveCard",
"body": [
{
"type": "ColumnSet",
"style": "emphasis",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"text": "Expense Approval",
"wrap": true
}
],
"width": "stretch",
"padding": "None"
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"horizontalAlignment": "Right",
"color": "Accent",
"text": "[#ER-093249-90](https://amdesigner.azurewebsites.net/)",
"wrap": true
}
],
"width": "stretch",
"padding": "None"
}
],
"padding": "Default",
"spacing": "None"
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"size": "Large",
"weight": "Bolder",
"text": "Expense report by Miguel garcia is pending your approval.",
"wrap": true
}
],
"padding": "Default",
"spacing": "None",
"separator": true
},
{
"type": "Container",
"id": "7d00f965-40bb-9fc3-ff7b-a9b82a09ead4",
"padding": "Default",
"items": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Image",
"style": "Person",
"url": "https://amdesigner.azurewebsites.net/samples/assets/Miguel_Garcia.png",
"size": "Small",
"altText": "Miguel Garcia Avatar"
}
],
"width": "auto",
"padding": "None"
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"text": "Miguel Garcia",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "None",
"color": "Light",
"text": "Program Manager",
"wrap": true,
"size": "Small"
},
{
"type": "TextBlock",
"spacing": "None",
"color": "Light",
"text": "Oslo-O365",
"wrap": true,
"size": "Small"
}
],
"width": "stretch",
"padding": "None"
}
],
"spacing": "None",
"padding": "None"
}
],
"spacing": "None",
"separator": true
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "*\"Please approve the expenses incurred during my trip to San Francisco for the annual developer conference.\"*",
"wrap": true
},
{
"type": "FactSet",
"id": "a46a50e6-11c3-27bb-349f-b40d820a7b83",
"facts": [
{
"title": "Total Amount:",
"value": "$1,473.35"
},
{
"title": "Request on:",
"value": "09/21/2019"
}
]
}
],
"padding": {
"top": "None",
"bottom": "Default",
"left": "Default",
"right": "Default"
},
"spacing": "None"
},
{
"type": "Container",
"spacing": "None",
"items": [
{
"type": "Container",
"id": "b149e1cc-0414-662b-6435-ad68d851bf67",
"padding": "Default",
"items": [
{
"type": "ColumnSet",
"spacing": "None",
"style": "emphasis",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "Activity History",
"wrap": true,
"spacing": "None"
}
],
"width": "stretch",
"spacing": "None",
"padding": "None"
},
{
"type": "Column",
"id": "ActivityHistoryChevronDown",
"items": [
{
"type": "Image",
"selectAction": {
"type": "Action.ToggleVisibility",
"id": "ExpandActivityHistory",
"title": "Expand Activity History",
"targetElements": [
"ActivityHistoryWrapper",
"ActivityHistoryChevronUp",
"ActivityHistoryChevronDown"
]
},
"url": "https://amdesigner.azurewebsites.net/samples/assets/Down.png",
"width": "20px",
"altText": "Expand",
"height": "20px"
}
],
"width": "auto",
"spacing": "None",
"padding": "None"
},
{
"type": "Column",
"id": "ActivityHistoryChevronUp",
"isVisible": false,
"items": [
{
"type": "Image",
"selectAction": {
"type": "Action.ToggleVisibility",
"id": "CollapseActivityHistory",
"title": "Collapse Activity History",
"targetElements": [
"ActivityHistoryWrapper",
"ActivityHistoryChevronUp",
"ActivityHistoryChevronDown"
]
},
"url": "https://amdesigner.azurewebsites.net/samples/assets/Up.png",
"width": "20px",
"altText": "Collapse",
"height": "20px"
}
],
"width": "auto",
"padding": "None"
}
],
"padding": "None"
}
],
"spacing": "None",
"style": "emphasis",
"separator": true,
"selectAction": {
"type": "Action.ToggleVisibility",
"targetElements": [
"ActivityHistoryWrapper",
"ActivityHistoryChevronUp",
"ActivityHistoryChevronDown"
],
"id": "ExpandActivityHistory",
"title": "Expand Activity History"
}
},
{
"type": "Container",
"id": "ActivityHistoryWrapper",
"isVisible": false,
"items": [
{
"type": "TextBlock",
"text": "[approved] Collin Ballinger on 02.09.2019",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "Small",
"text": "[approved] Kat Larrson on 03.09.2019",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "Small",
"text": "[approved] Daisy Phillips on 03.09.2019",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "Small",
"color": "Good",
"text": "[pending] Amanda Brady (you)",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "Small",
"color": "Light",
"text": "[pending] Kevin Sturgis",
"wrap": true
}
],
"padding": {
"top": "Default",
"bottom": "Large",
"left": "Default",
"right": "Default"
},
"spacing": "None",
"separator": true
}
],
"padding": "None",
"separator": true
},
{
"type": "Container",
"id": "c8472889-6d94-90e5-5311-3ede0967cd86",
"items": [
{
"type": "Container",
"spacing": "None",
"separator": true,
"items": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "Expense Details",
"wrap": true,
"spacing": "None"
}
],
"width": "stretch",
"padding": "None"
},
{
"type": "Column",
"id": "ExpenseDetailsChevronDown",
"items": [
{
"type": "Image",
"selectAction": {
"type": "Action.ToggleVisibility",
"id": "ExpandExpenseDetails",
"title": "Expand Expense Details",
"targetElements": [
"ExpenseDetailsWrapper",
"ExpenseDetailsChevronUp",
"ExpenseDetailsChevronDown"
]
},
"url": "https://amdesigner.azurewebsites.net/samples/assets/Down.png",
"width": "20px",
"altText": "Expand",
"height": "20px"
}
],
"width": "auto",
"padding": "None"
},
{
"type": "Column",
"id": "ExpenseDetailsChevronUp",
"isVisible": false,
"items": [
{
"type": "Image",
"selectAction": {
"type": "Action.ToggleVisibility",
"id": "CollapseExpenseDetails",
"title": "Collapse Expense Details",
"targetElements": [
"ExpenseDetailsWrapper",
"ExpenseDetailsChevronUp",
"ExpenseDetailsChevronDown"
]
},
"url": "https://amdesigner.azurewebsites.net/samples/assets/Up.png",
"width": "20px",
"altText": "Collapse",
"height": "20px"
}
],
"width": "auto",
"padding": "None"
}
],
"padding": "None"
}
],
"padding": "Default",
"style": "emphasis",
"selectAction": {
"type": "Action.ToggleVisibility",
"targetElements": [
"ExpenseDetailsWrapper",
"ExpenseDetailsChevronUp",
"ExpenseDetailsChevronDown"
],
"id": "ExpandExpenseDetails",
"title": "Expand Expense Details"
}
},
{
"type": "Container",
"id": "ExpenseDetailsWrapper",
"isVisible": false,
"items": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"horizontalAlignment": "Left",
"text": "1.",
"wrap": true,
"size": "Medium"
}
],
"width": "auto",
"padding": "None",
"spacing": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "9d60e7c6-611b-c2b9-b43e-af37c0154ad6",
"facts": [
{
"title": "Expense Category:",
"value": "Hotel"
},
{
"title": "Transaction Amount:",
"value": "228.12 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "274d7b95-17ae-ff6d-fa93-27568cf0fb2c",
"facts": [
{
"title": "Transaction date:",
"value": "07/31/2018"
},
{
"title": "Foreign Currency:",
"value": "228.12 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
}
],
"padding": "None"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"horizontalAlignment": "Left",
"text": "2.",
"wrap": true,
"size": "Medium"
}
],
"width": "auto",
"padding": "None",
"spacing": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "96be582e-4971-5a04-a892-e3a9dbf88d40",
"facts": [
{
"title": "Expense Category:",
"value": "Travel"
},
{
"title": "Transaction Amount:",
"value": "25.7 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "0e2b3791-6d26-c8e8-6b9a-73a41566a1b3",
"facts": [
{
"title": "Transaction date:",
"value": "08/02/2019"
},
{
"title": "Foreign Currency:",
"value": "228.12 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
}
],
"padding": "None",
"separator": true
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"horizontalAlignment": "Left",
"text": "3.",
"wrap": true,
"size": "Medium"
}
],
"width": "auto",
"padding": "None",
"spacing": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "ad183c8c-00b6-f723-977d-5e5fd58a8b16",
"facts": [
{
"title": "Expense Category:",
"value": "Hotel"
},
{
"title": "Transaction Amount:",
"value": "205.3 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "3ac7f96e-9e02-27eb-8c96-ce44c9f7c7b9",
"facts": [
{
"title": "Transaction date:",
"value": "08/02/2019"
},
{
"title": "Foreign Currency:",
"value": "205.3 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
}
],
"padding": "None",
"separator": true
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"horizontalAlignment": "Left",
"text": "4.",
"wrap": true,
"size": "Medium"
}
],
"width": "auto",
"padding": "None",
"spacing": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "dc1e22e7-d681-6d0f-5ac2-8ed95ba7b399",
"facts": [
{
"title": "Expense Category:",
"value": "Travel"
},
{
"title": "Transaction Amount:",
"value": "1198.33 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
},
{
"type": "Column",
"items": [
{
"type": "FactSet",
"id": "97e7396a-bbb8-9c2d-b03f-7da75c20c503",
"facts": [
{
"title": "Transaction date:",
"value": "09/02/2019"
},
{
"title": "Foreign Currency:",
"value": "1198.33 USD"
}
]
}
],
"width": "stretch",
"spacing": "Small",
"padding": "None"
}
],
"padding": "None",
"separator": true
}
],
"padding": "Default",
"separator": true,
"spacing": "None"
}
],
"padding": "None",
"separator": true,
"spacing": "None"
},
{
"type": "Container",
"id": "353b659f-b668-fac0-5b7f-5d2f1bdb46ac",
"padding": "Default",
"items": [
{
"type": "ActionSet",
"actions": [
{
"type": "Action.Http",
"id": "accept",
"title": "Accept",
"method": "POST",
"url": "https://www.microsoft.com",
"body": "{}",
"isPrimary": true,
"style": "positive"
},
{
"type": "Action.ShowCard",
"id": "e1487cbc-66b0-037e-cdc4-045fb7d8d0b8",
"title": "Reject",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"id": "Comment",
"placeholder": "Add a comment",
"isMultiline": true
},
{
"type": "ActionSet",
"id": "1e77f639-e5a8-320f-c6de-4291227db6b3",
"actions": [
{
"type": "Action.Http",
"id": "1ca3a888-ebfb-1feb-064b-928960616e52",
"title": "Submit",
"method": "POST",
"url": "https://www.microsoft.com",
"body": "{comment: {{Comment.value}}}"
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"fallbackText": "Unable to render the card",
"padding": "None"
}
}
],
"spacing": "None"
}
],
"spacing": "None",
"separator": true
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0",
"padding": "None"
}

为了测试我新建一个手工触发的Flow来发邮件,连接Office 365 Outlook连接器的账号要使用前面注册时用来发送Actional Message的邮箱,也就是notification账号,邮件内容主要为前面的JSON,需要修改的地方如下:

  1. JSON内容的前面需要加上 <script type="application/adaptivecard+json"> ,后面需要加上 </script>

使用Outlook Actionable Messages实现简单的审批功能

  1. 前面复制的JSON中中增加一个元素,名称为originator,值为注册Provider时候 Provider Id (originator) 字段的值。

使用Outlook Actionable Messages实现简单的审批功能

  1. Action元素中的url要改成我们前面新建flow的url,也必须是注册Provider中 Target URLs 中能匹配的值。

使用Outlook Actionable Messages实现简单的审批功能

  1. 因为Cloud flow没有身份验证,虽然你使用Postman不传身份信息可以调用,但是Actional Message中的action必须要增加类似如下的元素才行。否则点击Actional Message中按钮会报错:The remote endpoint returned an error (HTTP 401). Please try again later.
                        "headers": [
{
"name": "Authorization",
"value": ""
}
]

使用Outlook Actionable Messages实现简单的审批功能

Flow中发送邮件步骤截图如下,邮件内容使用代码模式进行编辑,内容就是我前面步骤修改后的JSON。

使用Outlook Actionable Messages实现简单的审批功能

然后我们发送一封邮件来看效果。

使用Outlook Actionable Messages实现简单的审批功能

点击Accept后出现提示成功:

使用Outlook Actionable Messages实现简单的审批功能

也可以点击Reject并输入Comment,然后点击Submit。

使用Outlook Actionable Messages实现简单的审批功能

我们看看Flow接收到这个Reject的内容如下:

使用Outlook Actionable Messages实现简单的审批功能

更多的定制,更加实际的场景留待以后博文继续讲解。