<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Power Automate on LinkeD365 Blog</title><link>https://linked365.blog/tags/power-automate/</link><description>Recent content in Power Automate on LinkeD365 Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 06 May 2021 00:00:00 +0000</lastBuildDate><atom:link href="https://linked365.blog/tags/power-automate/index.xml" rel="self" type="application/rss+xml"/><item><title>Using Power Automate and Graph API to manage External access to Teams</title><link>https://linked365.blog/2021/05/06/using-power-automate-and-graph-api-to-manage-external-access-to-teams/</link><pubDate>Thu, 06 May 2021 00:00:00 +0000</pubDate><guid>https://linked365.blog/2021/05/06/using-power-automate-and-graph-api-to-manage-external-access-to-teams/</guid><description>&lt;img src="https://linked365.blog/images/2021/05-image-18.png" alt="Featured image of post Using Power Automate and Graph API to manage External access to Teams" />&lt;p>&lt;a class="link" href="https://twitter.com/D365Geek" target="_blank" rel="noopener"
>MCJ&lt;/a> and I presented &lt;a class="link" href="https://d365uguk-22-apr-2021.sessionize.com/session/244458" target="_blank" rel="noopener"
>API’s – The most powerful tool, anyone can use!&lt;/a> for D365 UG and Swiss Power Saturday recently and I thought it would be good to share the Cloud Flow I created to automate the onboarding of external users to a Microsoft Team.&lt;/p>
&lt;p>This is usually an admin driven activity - A team owner will have to type in the email address for each guest they want to add, rather than something you can push to external users and allow them to request access.&lt;/p>
&lt;p>By using the Graph API via a Custom Connector, Microsoft Forms and Power Automate, we are able to realise this quickly and save a lot of time and effort in the manual process.&lt;/p>
&lt;p>Also, I have been lucky to have this solution accepted as part of the samples in the &lt;a class="link" href="https://pnp.github.io/" target="_blank" rel="noopener"
>Microsoft Patterns And Practices&lt;/a> initiative, so the Flow, Custom Connector and implementation instructions are available in GitHub &lt;a class="link" href="https://github.com/pnp/powerautomate-samples" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;h2 id="tldr">TLDR;&lt;/h2>
&lt;p>I created a Power Automate Cloud Flow which allows an external user to request access to a Team, get approved by the owner of the team, invite the external user to your tenant and add them to the team.&lt;/p>
&lt;h2 id="getting-hands-on-with-graph">Getting hands on with Graph&lt;/h2>
&lt;p>The main part of this flow is numerous calls to the &lt;a class="link" href="https://docs.microsoft.com/en-us/graph/overview" target="_blank" rel="noopener"
>Graph API&lt;/a>. The Graph API is Microsoft&amp;rsquo;s standard endpoint to expose and interact with data relevant to your tenant. It includes Microsoft 365 (such as Teams, Exchange, SharePoint, Workspace Analytics), Enterprise and Mobility and even Windows 10 activities and devices. It really should be seen as a single stop shop for anything and everything in your tenant. The only thing it doesn&amp;rsquo;t expose is D365 data.&lt;/p>
&lt;p>There is a &lt;a class="link" href="https://docs.microsoft.com/en-us/connectors/microsoftgraphsecurity/" target="_blank" rel="noopener"
>Graph connector&lt;/a> already available in Power Automate, but it is very limited to Security considerations. Thankfully, the rest of the Graph API abilities are available, but you have to go via a custom connector. You could call these directly via an HTTP request action, but by wrapping the Graph API in a connector, you are enabling other members of your organisation to re-use the connector and security you establish.&lt;/p>
&lt;p>Microsoft also allows you to &amp;ldquo;play&amp;rdquo; with the Graph API as well, via the &lt;a class="link" href="https://developer.microsoft.com/en-us/graph/graph-explorer" target="_blank" rel="noopener"
>Graph Explorer&lt;/a>. This web interface shows you all the sample calls you can make and also what permissions you require to call it and allows you to set up a call without using Power Automate or configuring a connector. It really should be the starting point for any Graph Customer Connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-2021-04-28_11-48-48-1-1024x520.png"
loading="lazy"
>&lt;/p>
&lt;p>If things go wrong, it is usually around the permissions. Within the Graph Explorer you can consent to these extra permissions on the fly, but more importantly, tells you what you need to configure in the permissions of your App registration to allow your connector the same access.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-2021-04-28_12-00-14-1024x548.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="granting-permissions">Granting Permissions&lt;/h2>
&lt;p>Now that we understand what permissions that are needed to get at the actions required, let&amp;rsquo;s jump in and create an &lt;a class="link" href="https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app" target="_blank" rel="noopener"
>Azure App Registration&lt;/a>. This allows you to grant rights to a particular application, which presents a client secret and App ID as part of the request. By doing this registration, you establish trust between your app (Custom Connector in our case) and your tenant.&lt;/p>
&lt;p>Navigate to &lt;a class="link" href="https://aad.portal.azure.com/" target="_blank" rel="noopener"
>https://aad.portal.azure.com&lt;/a> and log in. Select Azure Active Directory then App registrations. Select New, give it an appropriate name then hit Register.&lt;/p>
&lt;p>In the next screen, record the Application Id, going to need it later. On the left, select Certificates &amp;amp; secrets&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-2021-04-28_13-33-07-1-1024x364.png"
loading="lazy"
>&lt;/p>
&lt;p>Select New client secret give it a name &amp;amp; expiry date and select Save.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-2021-04-28_13-39-03-1.png"
loading="lazy"
>&lt;/p>
&lt;p>Ensure you copy the Value here, you will only be able to see this for a short while, if you navigate away it will be gone. Not a big deal, just recreate the client secret, but you will need it later. Now select API permissions. This will list what permissions this registration has, and by inference, the caller using the client secret/application id.&lt;/p>
&lt;p>Select the Add a permission button.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-2021-04-30_11-18-14-1024x484.png"
loading="lazy"
>&lt;/p>
&lt;p>This presents a choice of which API to expose. The first on the list is Microsoft Graph, the one we want.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>Select Delegated Permissions (Custom connectors doesnt support Application permissions yet, will run in the context of the person who runs the flow, so use an admin/system account) then type in the permission you want, listed here.&lt;/p>
&lt;ul>
&lt;li>Directory.ReadWrite.All&lt;/li>
&lt;li>User.Invite.All&lt;/li>
&lt;li>User.ReadWrite.All&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-8.png"
loading="lazy"
>&lt;/p>
&lt;p>Select Add Permissions to return you to the Configured permissions screen, then select Grant Admin consent button. This shortcuts and pre-approves the app.&lt;/p>
&lt;p>Leave this tab open and let&amp;rsquo;s go and define our custom connector.&lt;/p>
&lt;h2 id="defining-the-custom-connector">Defining the Custom Connector&lt;/h2>
&lt;p>Jan Bakker has done an excellent job of walking you through this, &lt;a class="link" href="https://powerusers.microsoft.com/t5/Power-Automate-Community-Blog/Build-a-custom-connector-for-Microsoft-Graph-API/ba-p/647492" target="_blank" rel="noopener"
>here&lt;/a>. His article goes into a lot of detail, so I will just take you through what is needed for this project.&lt;/p>
&lt;p>In make.powerapps.com, select the appropriate environment and chose Custom Connectors under Data. Then select New custom connector. You can import from various sources, but we want to create from blank, give it a name then fill out the next page.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-2021-04-30_12-00-33-1024x435.png"
loading="lazy"
>&lt;/p>
&lt;p>On the next screen, there are four tabs, which we need to step through. First the General Tab.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-10.png"
loading="lazy"
>&lt;/p>
&lt;p>Other stuff is nice to have and you &lt;em>should&lt;/em> document your work. Ensure Scheme is HTTPS and Host/Base URL are populated.&lt;/p>
&lt;ul>
&lt;li>Host - graph.microsoft.com&lt;/li>
&lt;li>Base URL - /v1.0&lt;/li>
&lt;/ul>
&lt;p>Select the Security Tab next, Authentication type is OAuth 2.0. You should be presented with the below screen.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-11.png"
loading="lazy"
>&lt;/p>
&lt;p>Now, this is where we need to use the values you saved (you did save them?) when you were registering your app up &lt;a class="link" href="#Granting-Permissions" >here&lt;/a>. Client Id is populated with the Application (client) ID, the Client secret is populated with the Value from the Client Secret grid. Login URL will be populated for you. The Resource URL value should be &lt;a class="link" href="https://graph.microsoft.com" target="_blank" rel="noopener"
>https://graph.microsoft.com&lt;/a>.Select Create Connector now, as you need the Redirect URL, created when you create the connector to put back into your App registration to complete the security process.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-12.png"
loading="lazy"
>&lt;/p>
&lt;p>I &lt;em>think&lt;/em> that this is now standard, but just in case, copy the Redirect URL and go back to your App Registration in Azure AD. Click on the link highlighted.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-2021-04-30_14-03-22-1024x347.png"
loading="lazy"
>&lt;/p>
&lt;p>Select Add a platform then chose Web. Enter the Redirect URL from the Custom Connector. Now we are ready to define the actions for our connector.&lt;/p>
&lt;h3 id="creating-the-actions">Creating the Actions&lt;/h3>
&lt;p>For our flow, we need 3 actions, Get the owners of a team, Get a user and Invite a User. This is where Graph Explorer and Postman help. You need to establish what you are sending and what you expect back for each action. For example, lets walk through the Get Owners of a team.&lt;/p>
&lt;p>Select New Action and Populate General section. The Operation ID needs to be unique and is what appears within Power Automate when you select it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-13.png"
loading="lazy"
>&lt;/p>
&lt;p>I usually use the same for each, but be creative and descriptive. Select Import from sample then use the url below as the URL and select Get as the Verb.&lt;/p>
&lt;p>URL - &lt;a class="link" href="https://graph.microsoft.com/v1.0/groups/%7bteamId%7d/owners" target="_blank" rel="noopener"
>https://graph.microsoft.com/v1.0/groups/{teamId}/owners&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-15.png"
loading="lazy"
>&lt;/p>
&lt;p>By placing teamid in curly brackets { } you denote to the custom connector you want to use a parameter in that URL. You can call &lt;a class="link" href="https://graph.microsoft.com/v1.0/groups/%7bteamId%7d" target="_blank" rel="noopener"
>https://graph.microsoft.com/v1.0/groups/{teamId}&lt;/a> (without the /owners) but that will return the detail from the group. In this I want get associated data, hence the /owners.&lt;/p>
&lt;p>Select Import. You are returned to the definition screen, where we can see that the request has been populated for us.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-16.png"
loading="lazy"
>&lt;/p>
&lt;p>We are ready to test our Get Owners now. Update the connector once more then head over to the Test tab. You will have to create a connection if not already done, which prompts you to establish who you are running the connector under (only for testing). Then supply a team id. These can be found by using Graph Explorer to find all teams. Hit the Test operation and if everything is working you will get a 200 response with some JSON in a body showing you all the information about the owners.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-17.png"
loading="lazy"
>&lt;/p>
&lt;p>To just define the others&lt;/p>
&lt;h4 id="get-user">Get User&lt;/h4>
&lt;p>Verb: Get, URL: &lt;a class="link" href="https://graph.microsoft.com/v1.0/users?$filter=" target="_blank" rel="noopener"
>https://graph.microsoft.com/v1.0/users?$filter=&lt;/a>&lt;/p>
&lt;h4 id="invite-user">Invite User&lt;/h4>
&lt;p>Verb: Put, URL: &lt;a class="link" href="https://graph.microsoft.com/v1.0/invitations" target="_blank" rel="noopener"
>https://graph.microsoft.com/v1.0/invitations&lt;/a>, Body is below, as you need to define a new invite, with the required parameters. The data doesn&amp;rsquo;t matter, just the parameters that you need to pass.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;invitedUserEmailAddress&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;emailaddress&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;inviteRedirectUrl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://myapp.contoso.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;invitedUserDisplayName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Testy McTest&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;sendInvitationMessage&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Make sure you test all your actions and lets move on to the Form.&lt;/p>
&lt;h2 id="microsoft-form-to-capture-information">Microsoft Form to capture information&lt;/h2>
&lt;p>Not going to dwell here, as others are doing a much better job at describing Forms. Basically, a simple form to define First and Last name, the email address and a choice field to define which team the user wants access to.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-18-1007x1024.png"
loading="lazy"
>&lt;/p>
&lt;p>The teams list will have to be maintained to those that you want the public to be able to request access to.&lt;/p>
&lt;p>Finally, lets take a look at the flow.&lt;/p>
&lt;h2 id="power-automate-definition">Power Automate Definition&lt;/h2>
&lt;p>This Flow is triggered by a new response being submitted against the Form defined above. Next, get the response details.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-19.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, retrieve all the Teams in your environment. This will return a JSON object which defines an array of Team definitions. We need to filter that to the one the user selected so that we can get the team id.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-20.png"
loading="lazy"
>&lt;/p>
&lt;p>To do this, I use the Filter Array action, pass in the output from the List Teams and ensure we select where team Name is equal to the team selected (the Which Team? field in my case).&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-21.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, is a compose statement. I do that just to simplify the way the flow works, as the return of filter array is an array, and I just want the first one.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-22.png"
loading="lazy"
>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">body(&amp;#39;Filter_array&amp;#39;)&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">?&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;id&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, lets get the Owners of the team selected, using the Id just retrieved. This is the first time using the Custom Connector, it is available under the Custom Tab.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-23.png"
loading="lazy"
>&lt;/p>
&lt;p>Selecting the connector will show the actions or triggers available.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-25.png"
loading="lazy"
>&lt;/p>
&lt;p>The parameters are those that were defined in the Custom connector, passing in the output from the compose above.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-26.png"
loading="lazy"
>&lt;/p>
&lt;p>Now, the response back from Get Owners is a JSON object, so next, Parse the JSON so there is a list of JSON objects for the flow to use. All that is needed in the approval that comes next is the email address(es) of the return from the owners call. But the approval needs a semi-colon separated list of emails. To achieve this, firstly use a Select to just return the email address from the JSON object, then join the output to that with a semi colon.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-27.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, start an approval. This is populated to let the owner know who has asked for access to the team and which team.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-28.png"
loading="lazy"
>&lt;/p>
&lt;p>Check whether the response is positive. If this was for production, I would probably send an email to the requesting user to let them know that they were denied access. You could also use the response written in the rejection.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-29-1024x171.png"
loading="lazy"
>&lt;/p>
&lt;p>In the Yes path, call the Custom connector again to check if the user is already a part of your organisation as a guest user. As the parameter is expecting a query, use the expression below&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-30.png"
loading="lazy"
>&lt;/p>
&lt;p>mail eq &amp;lsquo;Email Parameter from the Form response&amp;rsquo;&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">mail&lt;/span> &lt;span class="err">eq&lt;/span> &lt;span class="err">&amp;#39;Email&lt;/span> &lt;span class="err">Parameter&lt;/span> &lt;span class="err">from&lt;/span> &lt;span class="err">the&lt;/span> &lt;span class="err">Form&lt;/span> &lt;span class="err">response&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, check the length of the returned object from the custom connector. This basically checks if the user already belongs to your environment.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-32.png"
loading="lazy"
>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">length(outputs(&amp;#39;GetUser&amp;#39;)?[&amp;#39;body/value&amp;#39;])
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If there is a value in the return, use the return to invite the user to the team.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-33.png"
loading="lazy"
>&lt;/p>
&lt;p>The User id is returned by using the expression below&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">body(&amp;#39;GetUser&amp;#39;)?&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;value&amp;#39;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">?&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;id&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>On the negative side, firstly invite the user to your organisation by using the final action of the custom connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-34.png"
loading="lazy"
>&lt;/p>
&lt;p>And finally, use the response from your custom connector, the invited user to the team.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2021/05-image-35.png"
loading="lazy"
>&lt;/p>
&lt;p>Thats it! There is a lot of configuration here, but you can see how you can extend your usage of Power Automate to automate a function usually confined to manual work by the team owner.
&lt;style type="text/css">.notice{--root-color:#444;--root-background:#eff;--title-color:#fff;--title-background:#7bd;--warning-title:#c33;--warning-content:#fee;--info-title:#fb7;--info-content:#fec;--note-title:#6be;--note-content:#e7f2fa;--tip-title:#5a5;--tip-content:#efe}@media (prefers-color-scheme:dark){.notice{--root-color:#ddd;--root-background:#eff;--title-color:#fff;--title-background:#7bd;--warning-title:#800;--warning-content:#400;--info-title:#a50;--info-content:#420;--note-title:#069;--note-content:#023;--tip-title:#363;--tip-content:#121}}body.dark .notice{--root-color:#ddd;--root-background:#eff;--title-color:#fff;--title-background:#7bd;--warning-title:#800;--warning-content:#400;--info-title:#a50;--info-content:#420;--note-title:#069;--note-content:#023;--tip-title:#363;--tip-content:#121}.notice{padding:18px;line-height:24px;margin-bottom:24px;border-radius:4px;color:var(--root-color);background:var(--root-background)}.notice p:last-child{margin-bottom:0}.notice-title{margin:-18px -18px 12px;padding:4px 18px;border-radius:4px 4px 0 0;font-weight:700;color:var(--title-color);background:var(--title-background)}.notice.warning .notice-title{background:var(--warning-title)}.notice.warning{background:var(--warning-content)}.notice.info .notice-title{background:var(--info-title)}.notice.info{background:var(--info-content)}.notice.note .notice-title{background:var(--note-title)}.notice.note{background:var(--note-content)}.notice.tip .notice-title{background:var(--tip-title)}.notice.tip{background:var(--tip-content)}.icon-notice{display:inline-flex;align-self:center;margin-right:8px}.icon-notice img,.icon-notice svg{height:1em;width:1em;fill:currentColor}.icon-notice img,.icon-notice.baseline svg{top:.125em;position:relative}&lt;/style>
&lt;div>&lt;svg width="0" height="0" display="none" xmlns="http://www.w3.org/2000/svg">&lt;symbol id="tip-notice" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">&lt;path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>&lt;/symbol>&lt;symbol id="note-notice" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">&lt;path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>&lt;/symbol>&lt;symbol id="warning-notice" viewBox="0 0 576 512" preserveAspectRatio="xMidYMid meet">&lt;path d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>&lt;/symbol>&lt;symbol id="info-notice" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">&lt;path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/>&lt;/symbol>&lt;/svg>&lt;/div>&lt;div class="notice warning" >
&lt;p class="first notice-title">&lt;span class="icon-notice baseline">&lt;svg>&lt;use href="#warning-notice">&lt;/use>&lt;/svg>&lt;/span>Warning&lt;/p>&lt;p>Adding guest users to your Teams is only available if your administrator has configured it. This logic will not override that and will only produce errors if it has been disabled.&lt;/p>&lt;/div>
&lt;/p></description></item><item><title>Adding to Calendars using Power Automate</title><link>https://linked365.blog/2020/06/24/adding-to-calendars-using-power-automate/</link><pubDate>Wed, 24 Jun 2020 00:00:00 +0000</pubDate><guid>https://linked365.blog/2020/06/24/adding-to-calendars-using-power-automate/</guid><description>&lt;img src="https://linked365.blog/images/2020/06-image-81.png" alt="Featured image of post Adding to Calendars using Power Automate" />&lt;p>D365 Customer Service and Field Service both use Calendars associated with resources to schedule working time. This is accessible via the resource calendar and typically relies on the individual working a set time frame each week. Fiona works Monday to Friday, 8-5.&lt;/p>
&lt;p>When you are working with volunteers, this doesn&amp;rsquo;t work. Each volunteer will have an inconsistent schedule, with most of the time not being available. Rita has just volunteered for a couple of hours next Thursday.&lt;/p>
&lt;p>A recent client project to improve their scheduling of resources and was definitely in the second scenario. It wanted volunteers to sign up for times using simple time entry on a portal and transfer these times to the scheduling board.&lt;/p>
&lt;p>As Calendar is available as an entity, my initial thoughts that these would be a simple integration using Flow, unfortunately, not. This post details my findings and how I achieved creating sporadic &amp;ldquo;On Time&amp;rdquo; in Field Service or Customer Service.&lt;/p>
&lt;p>All credit for this discovery goes &lt;a class="link" href="https://www.linkedin.com/in/david-bostock-b097a730/" target="_blank" rel="noopener"
>Dave Bostock&lt;/a>, my colleague at Avanade, but judicious use of F12. Thanks to &lt;a class="link" href="https://twitter.com/JasonAAlmeida" target="_blank" rel="noopener"
>Jason Almeida&lt;/a> and &lt;a class="link" href="https://www.linkedin.com/in/robdawson/" target="_blank" rel="noopener"
>Rob Dawson&lt;/a> for their support and experience to nudge me to the solution.&lt;/p>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>How to create calendar entries in D365 for resources using an unbound action in Power Automate.&lt;/p>
&lt;h2 id="manual-configuration-of-calendar-entities">Manual configuration of Calendar Entities&lt;/h2>
&lt;p>Every bookable resource has a calendar associated with them. This Calendar has several rules which establish the working pattern for the resource. A rule, normally, would say &amp;ldquo;This resource works Monday to Friday, 9 to 5&amp;rdquo; or something similar.&lt;/p>
&lt;p>From the Resource, you can get to a calendar view by selecting Show Work Hours, highlighted below. It may also be available as a tab on the main form, depending on whether you are using Field Service or not. For me, I am still using the Customer Service version.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-74.png?fit=1024%2C602&amp;amp;ssl=1"
loading="lazy"
>&lt;/p>
&lt;p>Top left, you can alter the default rule for the user by adding a new weekly schedule, add an update to a single day or add time off for the resource.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-75.png"
loading="lazy"
>&lt;/p>
&lt;p>Selecting New Weekly Schedule and populating the next screen is the standard way resource get a weekly schedule.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-76.png"
loading="lazy"
>&lt;/p>
&lt;p>For volunteer management, resource tends to have a blank calendar. This is done by deleting all the calendars associated with the resource. From there, a volunteer would sign up for periods or shifts. This, on the face of it, just sounds like data entry, easily achieved via Powe Automate.&lt;/p>
&lt;h2 id="creating-rules-via-power-automate">Creating Rules via Power Automate&lt;/h2>
&lt;p>If you use either of the Common Data Service connectors to create a calendar rule, you will be presented with this error.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-77.png"
loading="lazy"
>&lt;/p>
&lt;p>This is because calendarrule is not a real entity, rather it is an aggregate entity. You can not create or update them directly. The only way you will get around this is to use the unbound action, msdyn_SaveCalendar.&lt;/p>
&lt;p>To demonstrate this, I have created a simple Schedule entity. This is a child of Contact, with 2 datetime fields, start and end. The premise is the Resource, via a portal, will be allowed to add to their schedule by signing up for predefined slots, but this will mean that there is a schedule for each contact.&lt;/p>
&lt;p>When a schedule record is created, we need to create the corresponding CalendarRule to show that this resource is available in the calendars used for planning.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-79.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, retrieve the calendar for this resource. The schedule is linked to a Contact, so use the Id to find the Bookable Resource.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-80.png"
loading="lazy"
>&lt;/p>
&lt;p>Finally, call the action. It is in a loop as the List records always returns 1 or more records.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-81.png"
loading="lazy"
>&lt;/p>
&lt;p>This is a JSON snippet, creating an object to add to the calendar.&lt;/p>
&lt;p>CalendarId is the Resources calendar, ObjectTypeCode defines this as a Bookable resource, displayed &lt;a class="link" href="http://www.dynamicscrm.blog/object-type-codes-cheat-sheet-for-dynamics-365/" target="_blank" rel="noopener"
>here&lt;/a>. TimeZoneCode is obvious, this points us to UTC. StartDate I have populated with the start of the schedule.&lt;/p>
&lt;p>The next is a list of rules to add to this Calendar, you could create more than one here, but we don&amp;rsquo;t have to. Start and End time are taken directly from the Schedule record. Duration is the time in minutes for the scheduled appointment. This can be calculated by firstly converting both to ticks (seconds) and reconverting to minutes. I got this from an excellent response to a forum question &lt;a class="link" href="https://powerusers.microsoft.com/t5/Building-Power-Apps/how-to-get-difference-between-two-dates-in-flow/td-p/283186" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">div&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">div&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mul&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ticks&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">triggerOutputs&lt;/span>&lt;span class="p">()?[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">cc_end&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">]),&lt;/span>&lt;span class="n">ticks&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">triggerOutputs&lt;/span>&lt;span class="p">()?[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">cc_start&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">])),&lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="m">1000000000&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="m">60&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Effort, timecode and subcode are required, defaulted to indicate a working period (timecode 0) and 100% effort (Effort 1).&lt;/p></description></item><item><title>Using Power Automate to generate Data</title><link>https://linked365.blog/2020/06/02/using-power-automate-to-generate-data/</link><pubDate>Tue, 02 Jun 2020 00:00:00 +0000</pubDate><guid>https://linked365.blog/2020/06/02/using-power-automate-to-generate-data/</guid><description>&lt;img src="https://linked365.blog/images/2020/06-alexander-sinn-KgLtFCgfC28-unsplash.jpg" alt="Featured image of post Using Power Automate to generate Data" />&lt;p>Previously, I have used &lt;a class="link" href="https://linked365.blog/2020/05/21/mockaroo-and-flow-perfect-demo-data/" target="_blank" rel="noopener"
>Mockaroo and Flow&lt;/a> to generate data, but the data I required for demoing the Customer Services Insights functionality has to be linked from existing data to be valid.&lt;/p>
&lt;p>In this post, I will guide you through the methods I have used to randomise and create demonstration data that fits the requirements for Customer Service Insights, just using Power Automate and an Excel file.&lt;/p>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Customer Service Insights only really works with data. This is great if you have some, but most demonstrations or trials have 10s of cases rather than 1000s. Here I walk through generating data using Flow, Rand() and Excel, mainly. Also, how to correct data if you didn&amp;rsquo;t quite get it the right first time 😊.&lt;/p>
&lt;h2 id="creating-cases">Creating Cases&lt;/h2>
&lt;p>Cases in D365 are linked to Contacts and Accounts and they need a title. As a bare minimum, this will create a case. But, for Customer Service Insights to work effectively, you need a few more parts.&lt;/p>
&lt;p>Origin is where the case came from, typically Twitter, Facebook, Phone, Email, Web etc. This allows for the segmentation of your customer base by source. Products link the case back to the faulty product or service that the case was about. A Priority is also needed to define the workload case for your staff or denote a superior service. Also, we need to know the satisfaction or the originator of the case with the resolution. This can be taken from a Forms Pro survey, for example.&lt;/p>
&lt;p>All these fields I have taken from the various parts of the Customer Service Insights reports and wanted to generate data with this populated to allow for an impressive and realistic visualisation in CSI.&lt;/p>
&lt;p>But I wanted to randomise the data, it is quite easy in Flow to create records, but to randomise them you need to choose from a selection, you could query the data in your system for each of these, but I found it easier to use a central store of the lookup data.&lt;/p>
&lt;h2 id="excel-for-random-data">Excel for Random Data&lt;/h2>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-237.png"
loading="lazy"
>&lt;/p>
&lt;p>In Excel, I created an OneDrive file and added a table. In this table, I added several columns with information to build up a case.&lt;/p>
&lt;p>&lt;strong>Topic Start&lt;/strong> - Combine 3 fields to make the title of the case, which allows for randomising the title but keeping some keywords to allow for Topic categorisation by CSI.&lt;/p>
&lt;p>&lt;strong>IdCol&lt;/strong> - this is the key for the table. Each valid row has a unique number so I can retrieve the row in Flow later.&lt;/p>
&lt;p>&lt;strong>Topic Main&lt;/strong> - This is the middle part of the Title field. I am actually using a list of drug names I found on generatedata.com. By using a list, I hope to be able to drive the Topic categorisation.&lt;/p>
&lt;p>&lt;strong>Topic End&lt;/strong> - The final part of the topic, just some random words I thought sounded like ends of case titles.&lt;/p>
&lt;p>&lt;strong>Contact&lt;/strong> - This is the GUID for all the active contacts in the system. I exported the list to excel, unhide the columns on the left and grabbed that list.&lt;/p>
&lt;p>&lt;strong>Channel&lt;/strong> - These are the numeric values for the optionset used for Channel. I drilled into the options set from the field on the case.&lt;/p>
&lt;p>&lt;strong>Priority&lt;/strong> - Again a numeric values for the Priority optionset.&lt;/p>
&lt;p>&lt;strong>Product&lt;/strong> - This is a list of the active Product Ids in the environment.&lt;/p>
&lt;h2 id="create-cases-flow">Create Cases Flow&lt;/h2>
&lt;p>The overall flow is quite lengthy and has several conditions. Let&amp;rsquo;s step through it from the top. The flow is available in my CustServInsights solution &lt;a class="link" href="https://github.com/CooksterC/Community/tree/master/CustServInsights" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;p>First of all, trigger the flow from a button. The input parameter denotes how many cases you would like to make. Ensure you keep this as 1 for testing. Also, initialise the case counter for use in the next loop.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, create a Do Until loop. This will keep looping until our case counter matches or exceeds the required number of cases.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-1.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="retrieve-the-random-data">Retrieve the random data&lt;/h3>
&lt;p>Next, get the relevant lines from that Excel file. For each of the fields, choose a random number from 1 and the maximum number for each data set and get the corresponding row in the Excel file. This is for Priority where I have 5 rows in the Excel sheet (I duplicate Normal which has a value 2 to weight the priorities in favour of Normal), hence the Rand function is&lt;/p>
&lt;p>Rand(1_&lt;the minimum it can be>&lt;em>, 6&lt;/em>&lt;the next integer after the maximum it can be>_)&lt;/p>
&lt;p>Rand (1,6) for Priority.&lt;/p>
&lt;p>The Key Column is the IdCol of the file, populated with integers&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-2.png"
loading="lazy"
>&lt;/p>
&lt;p>This action is repeated for each of the random columns in the Excel file.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-3.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="create-a-case">Create a case&lt;/h3>
&lt;p>Next, use these values to create a Case. Use the Create a new record action and populate with the appropriate values. Title uses a combination of each of the topic parts. Customer (Contacts) uses the confusing for citizen developers syntax to populate the contact on the case. This only applies if you are using the Current Environment version of the custom connector. There is an idea to revert this back to the previous syntax &lt;a class="link" href="https://powerusers.microsoft.com/t5/Power-Automate-Ideas/Allow-Setting-null-value-to-the-lookup-field-in-Common-Data/idi-p/427649" target="_blank" rel="noopener"
>here&lt;/a>. Please vote! I also added a description, just so I can retrieve these new cases later if I need to.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-5.png"
loading="lazy"
>&lt;/p>
&lt;p>Origin uses the number for the optionset returned by the Excel and converts to an integer. The product field uses the same syntax as Contact.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-6.png"
loading="lazy"
>&lt;/p>
&lt;p>Priority is converted to an integer as well. Record created on is used to alter the created date of the record. It is a pseudo change, as the record keeps both what you want to show and the actual date as separate fields in the CDS. Unfortunately, there is a bug in both CDS connectors and the D365 connector that strips out the time part of the field. I raised the bug &lt;a class="link" href="https://powerusers.microsoft.com/t5/I-Found-A-Bug/CDS-connectors-can-not-update-overriddencreatedon-field/m-p/578164" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>The compose used as a parameter is below. Use the rand function again to subtract up to 45 days from the current date and subtract up to 6 hours from the result. Finally, format the result to a date-time acceptable by the connector&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-8.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="complete-the-slas">Complete the SLAs&lt;/h3>
&lt;p>Once the record is created, randomise the closure as well as whether it met any SLAs in place for first-contact and closure. Firstly, generate another random number which will be used to define which path our case will take. Next, choose if this case is still open. Any value greater or equal to 8, so 8, 9 and 10 or 30% of the cases, should be still open.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-9.png"
loading="lazy"
>&lt;/p>
&lt;p>On the positive side, the case is still open, another condition to decide if the case was responded to in time. This effectively means 10% of the cases will have failed their First response SLA and are still open. 20% will be still open but the first response was passed. The positive side of this condition is empty, the negative side, update the SLAs to show the first response SLA is met.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-10.png?fit=1024%2C642&amp;amp;ssl=1"
loading="lazy"
>&lt;/p>
&lt;p>The first action in the negative side is a Update Record, using the Id of the created record. Update the first response field to Yes and the SLA succeeded to Yes. This is normally populated via workflow in the application.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-11.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-12.png"
loading="lazy"
>&lt;/p>
&lt;p>The next step is to retrieve all the SLA instances associated with the record. These are created when a case is created (if you have a default SLA configured) and denote when the SLA for that case will be breached. To make these appear as if the case passed its first response SLA, update the first response instance for this case. Firstly, find the record. This is a list records action, against the SLA KPI Instances entity, with a filter to retrieve only those that are for the case created. The order by condition means that the first response instance is returned first, as it&amp;rsquo;s failure time is naturally before the closure SLA failure time. Using the top count of 1 restricts the return to only one record. This should hopefully mean we retrieve 1 record only and that is the first response SLA.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-13.png"
loading="lazy"
>&lt;/p>
&lt;p>Loop through (there will only ever be one) the results and call an Update Record for each, marking the status as succeeded and populate the succeeded on date. Using the warning time will ensure it is prior to the failure time.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-14.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-15.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="closing-the-case">Closing the Case&lt;/h3>
&lt;p>On the negative side of the first condition statement, case is going to be closed, but first randomise whether this case met it&amp;rsquo;s case SLA. If the random number is 1 (10 % of cases) the case will be closed, but both the first response and case closure SLAs were not met.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-16.png"
loading="lazy"
>&lt;/p>
&lt;p>In the Yes part, use the same technique to the get the case SLA instances and update all of them to Non compliant.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-17.png"
loading="lazy"
>&lt;/p>
&lt;p>On the No side, make them all Succeeded and populate the date with the warning date, same as previous. The final part is to close the case. Whilst you now have the ability to perform an unbound action, I could not get it to work. Instead, I used a custom connector.&lt;/p>
&lt;h3 id="custom-connector-to-close-incident">Custom Connector to close Incident&lt;/h3>
&lt;p>I have numerous posts about configuring custom connectors, start with this article on &lt;a class="link" href="https://linked365.blog/2019/04/03/connecting-luis-d365-part-4-custom-connector/" target="_blank" rel="noopener"
>LUIS&lt;/a> to create one. The action is CloseIncident and defined like this.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-18.png"
loading="lazy"
>&lt;/p>
&lt;p>The body of the request is below.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;IncidentResolution&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;subject&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;test postman&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;incidentid@odata.bind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/incidents(7b978321-e7a0-ea11-a812-000d3a7fc8be)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;overriddencreatedon&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2020-05-22T13:28:43Z&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">-1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This is a post action against the CloseIncident action, the request is defined below.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-19.png"
loading="lazy"
>&lt;/p>
&lt;p>Drilling into the body parameter, you can see each of the parameters that are required.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-20.png"
loading="lazy"
>&lt;/p>
&lt;p>Using this in Flow is straight forward, add a new custom connector and populate the parameters. Be careful over the case Id, as it needs the odata binding populated.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-21.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="finishing-the-loop">Finishing the loop&lt;/h3>
&lt;p>The final part we do is increment the counter variable to ensure only the number of cases we specified are created.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-22.png"
loading="lazy"
>&lt;/p>
&lt;p>In terms of creating cases, that is it. I created nearly a thousand cases this way in 30 minutes (took a few hours to build out the code obviously) but this is reuseable and even 1000 records in 3 hours is not achievable by hand.&lt;/p>
&lt;h2 id="updating-cases-flow">Updating cases Flow&lt;/h2>
&lt;p>There is always a time when you forget something. In this scenario, Customer Services uses the CSAT score and whether the case is escalated, and my original flow didn&amp;rsquo;t include that information. In hindsight, add these values to the Create case flow. To correct, I created another flow.&lt;/p>
&lt;p>The trigger is the same, manual flow button. The first step is to retreive all the cases that I created. Remember I added to the description &amp;ldquo;This is a test case generated on UtcNow()&amp;rdquo;? This allows me to retrieve all the cases with a description that starts the same.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-23.png"
loading="lazy"
>&lt;/p>
&lt;p>Then I loop through the retrieved cases. First of all, create 2 random numbers for use later. Then check if case is closed.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-24.png"
loading="lazy"
>&lt;/p>
&lt;p>If a case is closed, all you can do is update the status and a few other fields, not all of them. If you try to update the Satisfaction on a closed case, you will get an error. So, for closed cases, open it first, by updating the record status and status reason.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-25.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-26.png"
loading="lazy"
>&lt;/p>
&lt;p>Then, update the record for a second time, this time adding the Satisfaction and Is Escalated flag. Satisfaction uses a formula to state if the random number, 1-6 is 5 or 6, use 5 as the rating, otherwise use the value of the random number. This allows for a weighting of the satisfaction emphasising a higher rating.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-27.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-28.png"
loading="lazy"
>&lt;/p>
&lt;p>Is escalated is true or false. In this scenario, I use a random again, so if the number is 5 or 6 using a range of 1-6, the case was escalated. This means only a small proportion is escalated.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/06-image-29.png"
loading="lazy"
>&lt;/p>
&lt;p>Now the case has the data required, close the case again with the custom connector used earlier.&lt;/p>
&lt;p>For the open case, side, just update the record, no need to open first.&lt;/p>
&lt;p>Header Photo by &lt;a class="link" href="https://unsplash.com/@swimstaralex?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="noopener"
>Alexander Sinn&lt;/a> on &lt;a class="link" href="https://unsplash.com/s/photos/data?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="noopener"
>Unsplash&lt;/a>&lt;/p></description></item><item><title>Mockaroo and Flow: Perfect Demo data</title><link>https://linked365.blog/2020/05/21/mockaroo-and-flow-perfect-demo-data/</link><pubDate>Thu, 21 May 2020 00:00:00 +0000</pubDate><guid>https://linked365.blog/2020/05/21/mockaroo-and-flow-perfect-demo-data/</guid><description>&lt;img src="https://linked365.blog/images/2020/05-luca-abad-lopez-qC6XbnBm-jo-unsplash-1.jpg" alt="Featured image of post Mockaroo and Flow: Perfect Demo data" />&lt;p>As a consultant, architect or just a blogger, you have to do a lot of demos. Recently, with my look at Sales Insights, the amount of data I needed was increasing. I had used Flow in the past to automate data entry, and it worked well, so I thought I would create some data for my environment using Flow, but first sourcing the data in Mockaroo.&lt;/p>
&lt;h2 id="mockaroo">Mockaroo&lt;/h2>
&lt;p>&lt;a class="link" href="https://mockaroo.com/" target="_blank" rel="noopener"
>Mockaroo&lt;/a> is a free data generator service which allows you to establish the fields you want and create a dummy import file for Dynamics. Great for my needs.&lt;/p>
&lt;p>For Sales Insights Lead prediction, I need Leads. So, First and last name, email, account name, topic, job title and email. Mockaroo has various types which you can use to flesh out the data. Fake company names, random first and last name etc. It does a great job of ensuring the chosen name / last name also links to the email address. Some things I needed were not present, such as the topic. It doesn&amp;rsquo;t really matter for my demo what data was in there, just needed something random, which the type grocery products gives me.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-147.png?fit=1024%2C823&amp;amp;ssl=1"
loading="lazy"
>&lt;/p>
&lt;p>Choose the number of rows you need, select the format as CSV and Download. Open in Excel to view your random data goodness.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-151.png"
loading="lazy"
>&lt;/p>
&lt;p>Importing data into Dynamics has never been easier. Select Import from CSV from the Import from Excel menu on a view for the entity you want to import against (assuming you have enabled Import Data in security) and choose your file. Mockaroo uses double quotes for its quotation mark and comma as a separator. Select Review Mapping.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-152.png?fit=499%2C1024&amp;amp;ssl=1"
loading="lazy"
>&lt;/p>
&lt;p>Here, map the CSV fields to the Dynamics fields, particular attention to the Primary fields which you need to populate. Hit Finish Import and this will queue your import job. Once successful, you will have a new set of &amp;ldquo;random&amp;rdquo; data for your use.&lt;/p>
&lt;h2 id="augmenting-your-data-in-flow">Augmenting your data in Flow&lt;/h2>
&lt;p>For Predictive Leads, you need 30 qualified and unqualified leads. To go through the steps to qualify each of the 1000 leads that were just added would be a time-consuming exercise. Also, there are some fields that the Predictive Leads default model would like, such as Industry type, budget, annual revenue and number of employees. These fields need updating with a random value prior to (dis)qualification.&lt;/p>
&lt;p>Industry type is an option set. The Common Data Service connector at the moment doesn&amp;rsquo;t give the user the ability to retrieve the values so you have to resort to a Custom Connector.&lt;/p>
&lt;p>I have been through &lt;a class="link" href="https://linked365.blog/2019/04/03/connecting-luis-d365-part-4-custom-connector/" target="_blank" rel="noopener"
>customer connectors&lt;/a> before, for my &lt;a class="link" href="https://linked365.blog/2019/10/16/d365-org-db-settings-canvas-app/" target="_blank" rel="noopener"
>Org Settings app&lt;/a> amongst others. A custom connector allows connection to the wider Power Platform API, where you can do all manner of things, but most importantly for us retrieve a list of options for a field.&lt;/p>
&lt;h3 id="connector-action-definition">Connector Action Definition&lt;/h3>
&lt;p>I am not going to go through the basics of the connector creation, see my previous posts for that. What is specific to this project is the action to retrieve the option set. The URL for retrieving a list of option set values is below.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">https&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="c1">//{url}/api/data/v9.1/EntityDefinitions(&amp;#39;{entity}&amp;#39;)/Attributes(&amp;#39;{field}&amp;#39;)/Microsoft.Dynamics.CRM.PicklistAttributeMetadata/OptionSet?$select=Options&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>{url} is just the standard Power Platform log in url. {Entity} and {field} select which entity and field you want to return the values for. For example, to return the gender code options on Contact, you would use this url.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">https&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="c1">//MYDEMOENVIRONMENT.crm11.dynamics.com/api/data/v9.1/EntityDefinitions(&amp;#39;contact&amp;#39;)/Attributes(&amp;#39;gendercode&amp;#39;)/Microsoft.Dynamics.CRM.PicklistAttributeMetadata/OptionSet?$select=Options&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Transposing to the requirement for a custom connector, allowing for path parameters.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">https://MYDEMOENVIRONMENT.crm11.dynamics.com/api/data/v9.1/EntityDefinitions(&amp;#39;{entity}&amp;#39;)/Attributes(&amp;#39;{field}&amp;#39;)/Microsoft.Dynamics.CRM.PicklistAttributeMetadata/OptionSet?$select=Options
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The {} are placeholders for path parameters. Use this link in the Import Sample and choose Get as the verb should result in the below action configuration.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-156.png"
loading="lazy"
>&lt;/p>
&lt;p>By editing the Query variable, you can make it default to Options, make it required and make it internal so it hides the value from your Flow call.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-158.png"
loading="lazy"
>&lt;/p>
&lt;p>This should be enough for you to start with your Flow.&lt;/p>
&lt;h3 id="update-leads-flow">Update Leads Flow&lt;/h3>
&lt;p>This flow is manually triggered. First thing is to get the options set values, which is a call to the custom connector. I want the Industry Types, so use the entity lead and the field industrycode.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-159.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, get all the leads you want to update. In my scenario, all the leads created after yesterday which is still active. This is anything I imported.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-160.png"
loading="lazy"
>&lt;/p>
&lt;p>Loop through the Leads and decide on whether it is going to be qualified or not. I do this by using the rand() function, which gives me a random integer between the first and second parameters. So, rand(1,4) would give me either 1, 2 or 3. As I want to favour qualified leads, only 1 counts as a lost deal. This will provide me with roughly 2/3 qualified leads.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-161.png?fit=1024%2C290&amp;amp;ssl=1"
loading="lazy"
>&lt;/p>
&lt;h4 id="disqualification-of-a-lead">Disqualification of a Lead&lt;/h4>
&lt;p>This is straight forward update record, but with a few interesting parts.&lt;/p>
&lt;p>Firstly, using rand() again populated budget, employee and annual revenue. Use different min and max values to populate the values accordingly.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-163.png"
loading="lazy"
>&lt;/p>
&lt;p>The only other interesting part is entering the Industry Type.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-164.png"
loading="lazy"
>&lt;/p>
&lt;p>The expression is below and broken down to the key parts.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">outputs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">OptionSetValues&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)?[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">Options&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">]?[&lt;/span>&lt;span class="n">rand&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">length&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">outputs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">OptionSetValues&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)?[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">/&lt;/span>&lt;span class="n">Options&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">]))]?[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Firstly get all the options return by my custom connector call.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">outputs(&amp;#39;OptionSetValues&amp;#39;)?[&amp;#39;body/Options&amp;#39;]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Next, select one of the options by choosing a random number between 0 (o indexed array) and the number of options returned. If I have 10 options, the list will be returned with items in slots 0 - 9. So this effectively chooses which one I am going to use by random.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">[rand(0, length(outputs(&amp;#39;OptionSetValues&amp;#39;)?[&amp;#39;body/Options&amp;#39;]))]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Finally, the Option is actually an object, the value I actually need to populated in the record is Value&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">?[&amp;#39;Value&amp;#39;]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I know, not exactly low code. There are other ways to do this, with Selects, etc., but this gives a one-line expression.&lt;/p>
&lt;p>Finally in this lead update, set the status and status reason.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-165.png"
loading="lazy"
>&lt;/p>
&lt;h4 id="qualifying-a-lead">Qualifying a Lead&lt;/h4>
&lt;p>Qualification of a lead is a little trickier. Start with a lead update, which is the same as the one for disqualification except you don&amp;rsquo;t set the status/status reason.&lt;/p>
&lt;p>Next, call the Perform an unbound action action. This is only available if you are within a solution and using the current environment connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-168.png"
loading="lazy"
>&lt;/p>
&lt;p>Select an entity, Lead, an Action, QualiifyLead and Item Id, the record that has just been updated. I have selected Yes for all the create options, so it will create a Contact, Account and Opportunity as it processes.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-170.png"
loading="lazy"
>&lt;/p>
&lt;p>At the bottom of the action, also update the status of the lead to 3, which is Qualified.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-169.png"
loading="lazy"
>&lt;/p>
&lt;p>The problem with this action is that it causes an error, the return from this action is an object where it is expecting an object. I am sure someone more skilled in the way of Flow will be able to tell me the real way of fixing this, but for me, I just add a Compose action that does nothing and ensures this action gets called whether the Qualification fails or not.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-171.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2020/05-image-172.png"
loading="lazy"
>&lt;/p>
&lt;p>The full flow is below. I manage to qualify or not 1000 leads in 27 minutes, admittedly it took a couple of hours to build the flow, but still, 2 1/2 hours for 1000 lead qualifications is pretty good!&lt;/p>
&lt;p>&lt;img src="https://i1.wp.com/linked365.blog/wp-content/uploads/2020/05/image-166.png?fit=1024%2C732&amp;amp;ssl=1"
loading="lazy"
>&lt;/p>
&lt;p>Hopefully this has showed you some of the tools available to you via Flow to manipulate data.&lt;/p>
&lt;p>Title Photo by &lt;a class="link" href="https://unsplash.com/@lucaca24?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="noopener"
>Luca Abad Lopez&lt;/a> on &lt;a class="link" href="https://unsplash.com/s/photos/kangaroo-disguise?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="noopener"
>Unsplash&lt;/a>&lt;/p></description></item><item><title>IFTTT - Stopping Freezer melt</title><link>https://linked365.blog/2019/10/07/ifttt-stopping-freezer-melt/</link><pubDate>Mon, 07 Oct 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/10/07/ifttt-stopping-freezer-melt/</guid><description>&lt;img src="https://linked365.blog/images/2019/10-image.png" alt="Featured image of post IFTTT - Stopping Freezer melt" />&lt;p>I have a problem, well my family has a problem. We can&amp;rsquo;t shut the freezer door. Regularly (once a month) our freezer would be left open just enough to defrost but not enough to sound the audible alarm, resulting in squishy food.&lt;/p>
&lt;p>Annoying.&lt;/p>
&lt;p>To combat this, I bought myself a &lt;a class="link" href="https://sonoff.tech/product/wifi-diy-smart-switches/th10-th16" target="_blank" rel="noopener"
>Sonoff TH10&lt;/a> temperature sensor and configured this to send me an alert when it reached a certain temperature. The problem is I became blase to the alert, as it would go off as soon as someone opened the door or was not around to react. The module would only alert me once, when a warm temperature was reached, not keep reminding me or escalate to someone else when I didnt react by shouting at a teenager to close the door. So how could I ensure it alerted me less frequently and also alert the rest of the family when there was a real issue?&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image.png?w=862"
loading="lazy"
>&lt;/p>
&lt;p>I know that this article is a little bit of fun with no real business benefit, but using Flow to fix a problem in my life is worth talking about. Everyone will have a little annoyance that Flow can help with, and it acts as a training exercise for us all.&lt;/p>
&lt;h2 id="design">Design&lt;/h2>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-1.png?w=458"
loading="lazy"
>&lt;/p>
&lt;p>As with all problems, starting with a flow chart helps. This is what I need to achieve. The Sonoff device is configured to turn on and off when a certain temperature is hit. IFTTT is triggered by Sonoff on both of these conditions.&lt;/p>
&lt;p>When the hot temperature is hit, wait 30 minutes (to allow for the temp change when a door is opened and closed naturally) then check if freezer is still is over temperature. If it is still hot, send out a notification.&lt;/p>
&lt;p>Then, wait 60 minutes now and check again if the temperature is too high, before repeating the loop, sending out a further notification and waiting 60 more minutes.&lt;/p>
&lt;p>If we check the temperature and it is now back down to cold, stop the process.&lt;/p>
&lt;h2 id="ifttt-configuration">IFTTT configuration&lt;/h2>
&lt;p>If you don&amp;rsquo;t know the &lt;a class="link" href="https://ifttt.com/" target="_blank" rel="noopener"
>IF This Then That,&lt;/a> it is a great service to interact with disparate systems, a point-to-point integration to do something on a trigger. I have used it previously to &lt;a class="link" href="https://linked365.blog/2019/05/04/ifttt-flow-d365/" target="_blank" rel="noopener"
>log Sales users calls&lt;/a> from an android phone and I use it to change my phone&amp;rsquo;s background with the NASA wallpaper of the day. It is a free service which plugs the gaps where Flow does not have direct connections, such as with the Sonoff device.&lt;/p>
&lt;p>Sonoff can trigger IFTTT, which in turn can trigger a web service, triggering the Flow.&lt;/p>
&lt;p>Log into IFTTT and link your account to eWeLink (this is the app you install to manage the Sonoff switches).&lt;/p>
&lt;p>Select Create from your account&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-2.png?w=455"
loading="lazy"
>&lt;/p>
&lt;p>This presents you with the standard IFTTT applet creator.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-3.png?w=1024"
loading="lazy"
>&lt;/p>
&lt;p>Hit the big + to display all the available services, eWeLink should be on that list.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-4.png?w=868"
loading="lazy"
>&lt;/p>
&lt;p>Next, you get a long list of the options eWeLink make available for you, this is where you need to select the appropriate switch, for me just a single channel switch.&lt;/p>
&lt;p>Next step is to select your switch, named by you when you configure it in the eWeLink app, and whether you want this to be triggered when it is switched On or Off. For me, On means that the Freezer has gone below -20 ° C. Off means it has gone above -17 ° C.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-5.png?w=513"
loading="lazy"
>&lt;/p>
&lt;p>IFTTT then needs to know what to do.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-6.png?w=1002"
loading="lazy"
>&lt;/p>
&lt;p>Hitting the big + again lists the action services, the one we want is a WebHook.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-7.png?w=815"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-8.png?w=353"
loading="lazy"
>&lt;/p>
&lt;p>Next, the action requires a URL, which is from our Flow, so skip to that bit to get the URL, we aren&amp;rsquo;t posting anything to this hook, just a trigger. You could secure this a little more to pass in a key to know it is you that is triggering it.&lt;/p>
&lt;h2 id="flow-configuration">Flow Configuration&lt;/h2>
&lt;p>Our flow starts with a web trigger.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-9.png?w=501"
loading="lazy"
>&lt;/p>
&lt;p>I am passing a delay to the trigger, though the initial call will not have any delay.&lt;/p>
&lt;p>Next, initialise a variable for checking the freezer later as well as an ID for the SharePoint Item I will create later.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-13.png?w=497"
loading="lazy"
>&lt;/p>
&lt;p>If this has come from the initial IFTTT trigger, then we assume the delay is 30 minutes. You could just update a delay variable, but as you will see, I want to do something different if I am passed a delay, the second time we call it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-11.png?w=533"
loading="lazy"
>&lt;/p>
&lt;p>In the yes branch, we now wait for the default 30 minutes.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-12.png?w=564"
loading="lazy"
>&lt;/p>
&lt;h2 id="cancelling-the-flow">Cancelling the Flow&lt;/h2>
&lt;p>Now, according to our design, we need to check the temperature to see if it has dropped back below our trigger point.&lt;/p>
&lt;p>Ideally, you would call the Sonoff and ask for a reading, and I am sure more expensive IoT devices allow you to do this, but I am cheap. The Sonoff and eWeLink interfaces allow you to look at the temperature on your phone, but do not broadcast the temperature out to anyone apart from the App, just the on / off trigger. Checking the temperature is not an option.&lt;/p>
&lt;p>What about creating another trigger to turn off this looping? Unfortunately, this isn&amp;rsquo;t possible; there is an idea on the flow community; you can vote for &lt;a class="link" href="https://powerusers.microsoft.com/t5/Flow-Ideas/Terminate-a-single-running-flow-from-Flow/idc-p/376465#M16692" target="_blank" rel="noopener"
>here&lt;/a>. I would also like to be proven wrong, reach out if there is a way to cancel a flow run.&lt;/p>
&lt;p>I started doing this by adding a file in a Google drive location (it could be SharePoint or OneDrive but wanted to expand my use of connectors) when the temperature went down, but this was inconsistent. I ended up getting multiple emails as the file create did not happen every time. I am sure I could sort this out, but wasn&amp;rsquo;t really the point of the flow.&lt;/p>
&lt;p>I ended up with creating an item in a SharePoint List when the flow was first triggered, complete the delay and check this item to see if another process has updated the same item while I was in pause mode. If that item has been updated, just stop the Flow. If it hadn&amp;rsquo;t send the email alert and trigger the flow again, to repeat the process.&lt;/p>
&lt;p>First, create the item in SharePoint, with the current time (this is just for my interest, knowing how many times it triggers the freezer temp)&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-15.png?w=514"
loading="lazy"
>&lt;/p>
&lt;p>The Id of the item that is created is copied so we can pass it to the next iteration of the flow if needed.&lt;/p>
&lt;p>After the delay, check the item that was created for a cold temperature. Using a bit of odata filtering here to only return the item that was just created and only if there is no cold time.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-16.png?w=522"
loading="lazy"
>&lt;/p>
&lt;p>This Scope useage is only to allow me to copy the contents to the other branch, saving me time. The copy function is really useful, if you havent seen it, why not?&lt;/p>
&lt;p>Next, if any items are returned (the item that was created was updated with a cold temperature), the title is changed to Cancelled. This is not really needed, but gave me something to do in the apply for each. The boolean is set to true, meaning Freezer is now cold and this flow can cancel itself.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-17.png?w=511"
loading="lazy"
>&lt;/p>
&lt;p>Out of the loop, check the value. If the previous loop did anything, it will be true, if not false. If the freezer is now cold, just terminate gracefully. If it has not updated the temperature, let it fall through to the next action.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-18.png?w=764"
loading="lazy"
>&lt;/p>
&lt;p>The final part of the original check to see if it sent a delay count is to email. In this no delay sent path, an email is just sent to me. I am using Gmail here, just because it was a new connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-19.png?w=517"
loading="lazy"
>&lt;/p>
&lt;p>Finally, call the same flow but pass in the delay we want and the Id of the SharePoint item that was created.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-22.png?w=494"
loading="lazy"
>&lt;/p>
&lt;h2 id="what-about-if-it-gets-really-serious">What about if it gets really serious?&lt;/h2>
&lt;p>Telling me if the freezer has been open is fine, it usually results in a yell to a teenager to shut the f**king door. What about if I am not in?&lt;/p>
&lt;p>After the first 1/2 hour delay, if the freezer is still showing hot, I need to escalate, for me means emailing my whole family. In the No branch, I delay for whatever value was sent in, set a variable to pass on to the next iteration and do the same code as in the other branch, the difference is that I include more people on the next email.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-23.png?w=522"
loading="lazy"
>&lt;/p>
&lt;p>Here, I email all my family, with a nice in your face subject and also hike up the importance. Hopefully, they won&amp;rsquo;t ignore this email like they ignore me.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-24.png?w=585"
loading="lazy"
>&lt;/p>
&lt;h2 id="triggering-when-it-is-cold">Triggering when it is cold&lt;/h2>
&lt;p>Using IFTTT the same as I have done early on, if the temperature hits -20 ° C, the second trigger is fired. This updates any items it finds in the list without a cold temperature with the trigger time&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-25.png?w=512"
loading="lazy"
>&lt;/p>
&lt;p>And that&amp;rsquo;s it.&lt;/p>
&lt;p>In my inbox, after I have taken the temperature controller out of the freezer for a little while, I get a series of Freezer is hot messages, also sent to the rest of the family.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-26.png?w=912"
loading="lazy"
>&lt;/p>
&lt;p>Once it gets back in, the notifications stop.&lt;/p>
&lt;p>And in SharePoint, there is a list of the hot &amp;amp; cold triggers, from natural door opening, thankfully&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/10-image-27.png?w=1005"
loading="lazy"
>&lt;/p></description></item><item><title>Cloning Flows: Location triggers for everyone</title><link>https://linked365.blog/2019/09/14/cloning-flows-location-triggers-for-everyone/</link><pubDate>Sat, 14 Sep 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/09/14/cloning-flows-location-triggers-for-everyone/</guid><description>&lt;img src="https://linked365.blog/images/2019/09-image-69.png" alt="Featured image of post Cloning Flows: Location triggers for everyone" />&lt;p>Sometimes ideas don&amp;rsquo;t work out. This is one of these times. But the reason I blog is to learn, expand my knowledge of the PowerPlatform, expand my knowledge of components outside of it. So, I figured I would blog about my failure, learning is learning. As I started testing the flow again, moving environments etc, it started working. I guess this is down to the location trigger being a work in progress. Moral of the story: If it is broke last month, try again this month.&lt;/p>
&lt;p>Back in July, I started working on this scenario, but couldn&amp;rsquo;t get it working. I noticed &lt;a class="link" href="https://twitter.com/Flow_joe_" target="_blank" rel="noopener"
>@Flow_Joe_&lt;/a> &amp;amp; &lt;a class="link" href="http://twitter.com/JonJLevesque" target="_blank" rel="noopener"
>@JonJLevesque&lt;/a> did a &lt;a class="link" href="https://www.youtube.com/watch?v=J0vZCotp9Pw&amp;amp;t=1493s" target="_blank" rel="noopener"
>video walkthrough&lt;/a> of using the Geofence trigger to send out a summary of the customer when a sales person enters an area, which reminded me of my failure, hence why I have written it up. While from Joe &amp;amp; Jon&amp;rsquo;s video shows us how easy it is to create a flow, for Salespeople in general, I think this is too far. You can not expect a Salesperson to have any interest in creating flows to do this, you can expect them to click a button on a form within their D365 application.&lt;/p>
&lt;h2 id="objectives">Objectives&lt;/h2>
&lt;ul>
&lt;li>The Scenario&lt;/li>
&lt;li>Creating the Flow button&lt;/li>
&lt;li>Cloning the Flow&lt;/li>
&lt;li>Outcome&lt;/li>
&lt;/ul>
&lt;h2 id="the-scenario">The Scenario&lt;/h2>
&lt;p>Numerous times when I have been a customer, a salesperson would come to us not knowing that we have several major cases logged with them against their product. This is mainly down to lazy sales people (I know, they don&amp;rsquo;t exist), but it would be awesome for the salesperson to get a summary of the account when they get in the door of a customer. The number of support cases, a list of the open opportunities and orders, any complaints that have been logged. All of this information is available to the salesperson via the D365 mobile app, but it would be good to ensure that they get this information and are less likely to get caught out by a customer venting at them for 5 critical bugs that have been sat around for a month.&lt;/p>
&lt;h2 id="the-solution">The Solution&lt;/h2>
&lt;p>Flow has a new trigger, still in preview, Location, which is triggered via the Flow application when an user enters or exists an area. This is perfect for our scenario, stick a GeoFence around a customers location, when the user enters the area, it gets triggered. Look up the Customer, format an email and send it to the user.&lt;/p>
&lt;p>Flow is user friendly, a low code solution, but you can not expect a salesperson to create a flow for each account they want to create this trigger for. What can be done, is put a button on a form, automatically create a Flow for the user against the account they have selected which would then be triggered when the user enters the location.&lt;/p>
&lt;p>There are 2 separate series of flows that are required, firstly to start with an action from the user on the account record, which triggers cloning of a template.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-31.png"
loading="lazy"
>&lt;/p>
&lt;p>The second series is the clone of the template, which triggers sending the salesperson the relevant information when they enter the customers property.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-32.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="creating-a-flow-button">Creating a Flow Button&lt;/h2>
&lt;p>Starting with a CDS &amp;ldquo;When a record is selected&amp;rdquo; trigger, configure it to be used when an account is selected.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-33.png?w=514"
loading="lazy"
>&lt;/p>
&lt;p>The next step is to retrieve who is running this flow. As mentioned, it will publish this button on a Account form, so it is essential to know who is running this, so an email can be sent to them. The account information and who the user is is sent as the body to a HTTP Post trigger, which is the next flow in the chain.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-39.png?w=502"
loading="lazy"
>&lt;/p>
&lt;p>An HTTP trigger is used because the next Flow requires enhanced access. An admin user needs to clone a Flow, which you would not want a normal user to be able to do. The admin is used as well to ensure any runs that happen are legitimate. The admin or sys account shouldn&amp;rsquo;t belong to someone who could have Flow in their pocket.&lt;/p>
&lt;p>To have the URL to send to, the next Flow needs to be created first, but just to show where this button appears within the D365 interface. The first time we run it, there are few confirmations that you need to do, finally you can run the flow.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-button-on-account.gif?w=1024"
loading="lazy"
>&lt;/p>
&lt;h2 id="cloning-the-flow">Cloning the Flow&lt;/h2>
&lt;p>This flow clones an existing template, tweak it slightly and gets it up and running as the user.&lt;/p>
&lt;p>Starting with an HTTP Trigger, I use a sample payload to build the schema.&lt;/p>
&lt;p>Next is retrieving the account. As the account id is passed in from the calling Flow, a simple Get Record is used.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-40.png?w=516"
loading="lazy"
>&lt;/p>
&lt;p>Next, configure the name of the Flow that will be created, making it unique for the user by adding their email address in. A flow definition string is also initialised for later&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-41.png?w=546"
loading="lazy"
>&lt;/p>
&lt;p>In this Flow, the user that called it from the button is needed, so it retrieves the profile using the Office 365 Users action.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-43.png?w=539"
loading="lazy"
>&lt;/p>
&lt;p>Next, retrieve my template flow. Flow has several actions around management of Flows, which are incredibly useful to a Flow administrator. The template flow is a simple flow which has a location trigger and a call to a http trigger to call a secondary flow. I will discuss later the detail about this.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-44.png?w=502"
loading="lazy"
>&lt;/p>
&lt;p>The next couple of actions try to determine if a flow with the FlowName defined already exists, firstly by getting a list of all my flows (as an admin) then getting a list of Flows in the organisation, then filtering it with the flowname that was defined in the initial steps&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-45.png?w=701"
loading="lazy"
>&lt;/p>
&lt;p>If there is a flow already, just stop. If not, carry on &amp;amp; clone the template flow.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-46.png?w=1024"
loading="lazy"
>&lt;/p>
&lt;h2 id="the-template">The Template&lt;/h2>
&lt;p>The Log Template is a very easy, small location trigger with an HTTP call action. The HTTP call passes in the user&amp;rsquo;s location and the account id and the user who started the process. Both email and account will be swapped out as part of the clone.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-63.png?w=523"
loading="lazy"
>&lt;/p>
&lt;p>The trigger region is essential for any location trigger. It triggers this one of the Microsoft campus in Redmond. Someday I will be fortunate to go to the motherland. I chose this as it is not likely that the user would have them as a client, but it doesn&amp;rsquo;t really matter where you chose, as what you need is the latitude and longitude from it so you can replace it when you clone the flow.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-48.png?w=1024"
loading="lazy"
>&lt;/p>
&lt;p>If you click on the peek code button against the trigger, it shows a JSON representation of the trigger. The latitude and longitude are that of the Microsoft office and this is the bit I need to replace&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-50.png?w=491"
loading="lazy"
>&lt;/p>
&lt;h2 id="cloning-the-flow-part-2">Cloning the Flow (part 2)&lt;/h2>
&lt;p>All a Flow is a JSON file. Obviously, how it is rendered and how the hooks and actions work are the power, but the definition is a JSON file. Using this knowledge, we can create a new version of the template, with a location specific to the account.&lt;/p>
&lt;p>The template in all it&amp;rsquo;s glory is below. Just using simple find / replace, we tweak it to the specific location, account and user.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;$schema&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contentVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1.0.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;$authentication&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;defaultValue&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;SecureObject&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;triggers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;manual&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Request&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Geofence&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;inputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;serializedGeofence&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Circle&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;latitude&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">47.64343469631714&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;longitude&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">-122.14205669389771&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;radius&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">35&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;actions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;HTTP&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;runAfter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Initialize\_Email\_Variable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Succeeded&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Http&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;inputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;uri&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://prod-68.westeurope.logic.azure.com:443/workflows/&amp;lt;GUID&amp;gt;/triggers/manual/paths/invoke?api-version=2016-06-01&amp;amp;sp=%2Ftriggers%2Fmanual%2Frun&amp;amp;sv=1.0&amp;amp;sig=&amp;lt;SIG&amp;gt;-JQQvYT0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;body&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;lat&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;@{triggerBody()?\[&amp;#39;currentLatitude&amp;#39;\]}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;long&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;@{triggerBody()?\[&amp;#39;currentLongitude&amp;#39;\]}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;@{variables(&amp;#39;Email&amp;#39;)}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;account&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;@{variables(&amp;#39;accountId&amp;#39;)}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Initialize\_Account\_Variable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;runAfter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;InitializeVariable&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;inputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;variables&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;accountId&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;String&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;accountId&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Initialize\_Email\_Variable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;runAfter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Initialize\_Account\_Variable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Succeeded&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;InitializeVariable&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;inputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;variables&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Email&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;String&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;email&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;outputs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Back on the clone flow, the next step is to convert the template to a string. This makes it easier to replace the latitude, longitude etc. with the ones we want.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-51.png?w=488"
loading="lazy"
>&lt;/p>
&lt;p>On the account OOTB record there is a latitude and longitude. This data is not normally populated, but it is used by Field Service and other applications. I used Field Service to populate it using the Geo Code button.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-52.png?w=356"
loading="lazy"
>&lt;/p>
&lt;p>As you can see from the above, Field service populates both latitude and longitude to 5 decimal places. This is common precision when you use any mapping software such as Google. so I am not sure why if you do the same by the Flow trigger you get precision to 15 dp for latitude and 17 for longitude.&lt;/p>
&lt;p>The next 2 steps are because of me trying to get the flow to work. One of my thoughts was that the flow was expecting the all 15 of the decimal places to be populated, so these steps pad out the number you have against the account with additional numbers.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-53.png?w=517"
loading="lazy"
>&lt;/p>
&lt;p>The expression is the same for both&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">concat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">body&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Get&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Account&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">address1&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_latitude&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="mi">111111&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The next step replaces the newly calculated values for latitude and longitude in the JSON definition&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-54.png?w=500"
loading="lazy"
>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">replace&lt;/span>&lt;span class="p">(&lt;/span> &lt;span class="nf">variables&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">flowdefstring&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="mf">47.643434696317136&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nf">outputs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Replace&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Lat&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)),&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">122.14205669389771&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nf">outputs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Replace&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Long&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The accountid is also replaced. This is used in the cloned flow to define which account the user selected. The trigger only gives you the user&amp;rsquo;s current location, not the centre of the circle you configured. You could use these values &amp;amp; find the account, with difficulty, unless there is something I am missing. I prefer to add a variable in the clone, which is the account id.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-60.png?w=486"
loading="lazy"
>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">outputs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Replace&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Lat&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Long&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">accountId&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nf">triggerBody&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Account&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The same with the email to send to, it should be the user who triggers the geofence, but seems to be the person who is the admin. As I clone the Flow with an admin account then add the user as an admin, it runs under the admin account.&lt;/p>
&lt;p>There is enough info now to create this flow. Using the Create Flow action, the new flow is created and up and running.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-68.png?w=497"
loading="lazy"
>&lt;/p>
&lt;p>I use a JSON expression to convert the string I have used to find / replace the latitude, longitude etc. to ensure the Flow is created with JSON.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">json&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">variables&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">flowdefstring&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The final step is to add a Flow owner. As the sales person who triggered the flow is who it should trigger on, make them the owner, so it should run under their context.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-56.png?w=524"
loading="lazy"
>&lt;/p>
&lt;h2 id="outcome-v1">Outcome V1&lt;/h2>
&lt;h4 id="ignore-this-bit-if-you-want-to-avoid-the-author-moaning-about-stuff-that-doesnt-work">Ignore this bit if you want to avoid the author moaning about stuff that doesn&amp;rsquo;t work.&lt;/h4>
&lt;p>If I run the whole flow, I do generate a new Flow.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-57.png?w=912"
loading="lazy"
>&lt;/p>
&lt;p>Going into what was generated, using peek code again, you can see that the Microsoft location has been replaced with the Avanade office&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-58.png?w=630"
loading="lazy"
>&lt;/p>
&lt;p>The trigger is active, but this is where it stops. I can not get this to trigger to fire. Changing the location to my home, going for a walk, coming back doesn&amp;rsquo;t trigger it.&lt;/p>
&lt;p>If I don&amp;rsquo;t put in the padding for the latitude and longitude, it doesnt trigger.&lt;/p>
&lt;p>If I clone from my location, not changing the latitude and longitude, still the trigger doesn&amp;rsquo;t fire.&lt;/p>
&lt;p>If I configure a new trigger from scratch, that works.&lt;/p>
&lt;p>Everything about the trigger look the same when you get it in code view, but there must be something different.&lt;/p>
&lt;p>This is where I started reaching out, I &lt;a class="link" href="https://twitter.com/linked365/status/1153218060632952834" target="_blank" rel="noopener"
>tweeted&lt;/a> about it to the gods of flow and asked in the Flow &lt;a class="link" href="https://powerusers.microsoft.com/t5/Building-Flows/Cloning-a-flow-Location-Trigger/m-p/330343" target="_blank" rel="noopener"
>forum&lt;/a> where I did get a response, basically saying the same, and that the location trigger is in preview.&lt;/p>
&lt;p>So, if you have got this far, how do I fix it?&lt;/p>
&lt;h2 id="outcome-v2">Outcome V2&lt;/h2>
&lt;p>Like I said at the outset, this didn&amp;rsquo;t work for me. Frustration set in, and I forgot the idea. But, as I was putting together this blog post, re-deploying the components as my demo system had expired, it worked!&lt;/p>
&lt;p>So, moving on, we need to sent an email to the user with the playbook for the account. I want to list the last 5 critical cases, last 5 open opportunities, last 5 notes and any description the user has put in.&lt;/p>
&lt;p>It triggers an HTTP request, the schema defined by a sample payload, but contains who triggered the workflow and which account.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-62.png?w=501"
loading="lazy"
>&lt;/p>
&lt;p>Then, a great time for a parallel branch. The Flow retrieves the cases, notes and opportunities in a parallel branch.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-64.png?w=1024"
loading="lazy"
>&lt;/p>
&lt;p>Each branch does a similar thing, looking at the Notes branch, firstly retrieve the records with a CDS List Records action, using an OData filter and order by, return the top 5 only.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-65.png?w=497"
loading="lazy"
>&lt;/p>
&lt;p>Next, put this in an HTML table, selecting the output from the Get Notes action. I select Advanced option, then Custom columns, this way I can define the order and which columns I want to display.&lt;/p>
&lt;p>The final step is to send an email&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-66.png?w=494"
loading="lazy"
>&lt;/p>
&lt;p>Obviously, this can be customised to your business need, but my example list the cases, opportunities &amp;amp; notes, and reminds them to fill in a contact report.&lt;/p>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>So, the user selects a button on an account form, which allows them to receive updates about one of their customers when they enter the location of the account. Easy.&lt;/p>
&lt;p>I tested this with my home address and with a different user and you can see that I get the email through. Veronica is in the US, I wasn&amp;rsquo;t up at 1am writing blogs &amp;amp; fixing Flows.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-67.png?w=1024"
loading="lazy"
>&lt;/p>
&lt;p>You can also see that Flow notifies the user that it has made them an administrator on a Flow.&lt;/p>
&lt;p>This Flow starts with a Flow button on a record, making it a user-initiated process. It could be triggered off a record creation process - If the user follows an Account, create this automation for them, as long as they have opted in.&lt;/p>
&lt;p>There is location tracking in the Field Service application, but that requires the Field Service mobile app and not suited to a Sales person. They just need to install the Flow app on their device and forget it is there.&lt;/p></description></item><item><title>AI Builder - Text AI</title><link>https://linked365.blog/2019/09/05/ai-builder-text-ai/</link><pubDate>Thu, 05 Sep 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/09/05/ai-builder-text-ai/</guid><description>&lt;img src="https://linked365.blog/images/2019/09-image-30.png" alt="Featured image of post AI Builder - Text AI" />&lt;p>My blogging journey started with using &lt;a class="link" href="https://www.luis.ai" target="_blank" rel="noopener"
>LUIS&lt;/a>, one of Microsoft&amp;rsquo;s Cognitive Services to automate case assignment. This &lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365-part-1/" >blog&lt;/a> goes into detail about how this all hung together, using a model defined in LUIS, calling the LUIS endpoint when a new cases are created and classifying the case, by the subject, with the result from the call.&lt;/p>
&lt;p>After my summer break (sorry, but family etc comes first) I thought I would revisit this scenario, but using one of Microsoft&amp;rsquo;s shiny, new AI Builder capabilities, &lt;a class="link" href="https://docs.microsoft.com/en-us/ai-builder/text-classification-overview" target="_blank" rel="noopener"
>Text Classification AI Model&lt;/a>.&lt;/p>
&lt;h2 id="objectives">Objectives&lt;/h2>
&lt;ul>
&lt;li>The Scenario&lt;/li>
&lt;li>Training your Model&lt;/li>
&lt;li>Getting Data&lt;/li>
&lt;li>Publishing the Model&lt;/li>
&lt;li>Using the Tags&lt;/li>
&lt;/ul>
&lt;h2 id="the-scenario">The Scenario&lt;/h2>
&lt;p>In my first blog, I went through the scenario, so not wanting to repeat myself, but for the lazy who don&amp;rsquo;t want to click &lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365-part-1/" >through&lt;/a>&amp;hellip;..&lt;/p>
&lt;p>Big Energy is a supplier of energy products to end users. They have a call centre which handles any query form the customer. As a perceived leader in the sector, it is always wiling to use the latest technology to allow users to interact with them, which reduces the pressure on the customer support centre.&lt;/p>
&lt;p>Big Energy has a mail box configured to accept customer emails about anything and, rather than have a group of 1st line support employees filtering out and categorising the emails based on the content, want to use cognitive services to improve the process of getting the email (the generated case) to the right team.&lt;/p>
&lt;h2 id="using-ai-to-file-the-case">Using AI to file the case&lt;/h2>
&lt;p>LUIS does a great job of this, with a BA providing sample utterances for the model and training it.&lt;/p>
&lt;p>Text Classification AI Model does it slightly differently. The model expects users to provide data (in the CDS) in the form of text blocks and representative tags for the data. Both need to be in the same entity in CDS.&lt;/p>
&lt;p>On a standard Case record, the classification or tag is the subject field. This is a parent record of Case and the tag would be the name of the subject. As subject and case are separate entities, the Text Classification AI model will not work. A field, be it a calculated one, has to be introduced to enable the classification AI to work. Adding data to an entity from a parent entity breaks my &lt;a class="link" href="https://en.wikipedia.org/wiki/Database_normalization" target="_blank" rel="noopener"
>Third Normal Form&lt;/a> training (anyone remember that? Is it still a thing?).&lt;/p>
&lt;p>I have raised this issue as a new &lt;a class="link" href="https://powerusers.microsoft.com/t5/PowerApps-Ideas/Parent-fields-available-as-tags-for-Text-Classification/idi-p/354238" target="_blank" rel="noopener"
>idea&lt;/a> on the PowerApps ideas forum, go there and give it a vote!&lt;/p>
&lt;p>The new logic for our AI model is that the AI will classify the incoming case, adding a tag. This will trigger a flow, changing the subject of the linked case accordingly. This will trigger re-routing of the case like it did in the original LUIS method.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-30.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="training-your-ai">Training your AI&lt;/h2>
&lt;p>With any AI model, it needs training. The AI needs to separate the wheat from the chaff. Creating a model is simple in PowerApps.&lt;/p>
&lt;p>Start at &lt;a class="link" href="https://make.powerapps.com" target="_blank" rel="noopener"
>make.powerapps.com&lt;/a> and select AI Builder, then Build&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-1.png"
loading="lazy"
>&lt;/p>
&lt;p>There are 4 options here&lt;/p>
&lt;p>Binary Classification is useful to give a yes / no decision on whether data meets certain criteria. The criteria can be up to 55 fields on the same entity. For example, is a lead with a low current credit limit, high current account value, no kids but has a pink toe nail (shout out to &lt;a class="link" href="https://twitter.com/Themarkchristie" target="_blank" rel="noopener"
>Mark Christie&lt;/a>) likely to get approved for a new loan?&lt;/p>
&lt;p>Form processing is intended to assist users in automated scanned documents to prevent re-keying. An example would be any forms hand written as part of a sales or service process (before you convert to a PowerApp obviously).&lt;/p>
&lt;p>Object detection assists in classification of items, be in types of drink, crisps or bikes, etc.&lt;/p>
&lt;p>Text classification decides on a tag for a block of text, for example, a user could enter a review of a product online and text classification could understand what product it was for or whether it is a positive review.&lt;/p>
&lt;p>All 4 of these have origins in the Cognitive services provided by Azure, LUIS being the big brother of Text Classification.&lt;/p>
&lt;p>Ensure you are in the correct environment. Text Classification only works on data within your CDS environment, so don&amp;rsquo;t expect to reach out to your on-premise SQL server. There are ways to bring data into CDS, not in scope for this discussion.&lt;/p>
&lt;p>Selecting Text Classification displays a form to give you more understanding, and it is here that you name your model&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-2.png"
loading="lazy"
>&lt;/p>
&lt;p>Hit Create and then Select Text. This will list all your entities in your CDS environment (in my case, a D365 demo environment).&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-3.png"
loading="lazy"
>&lt;/p>
&lt;p>Select the entity you want, Case for our PoC.&lt;/p>
&lt;p>The interface will then list all the fields suitable for the AI model, namely anything that is a text field. I chose the description field, which is typically the email that the user enters when emailing in a case to the support department.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-4.png"
loading="lazy"
>&lt;/p>
&lt;p>Hit the Select Field button and it will present you with a preview of the data in that table.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-5.png"
loading="lazy"
>&lt;/p>
&lt;p>The next screen is to select your tags. This needs to be in the same table, and as already discussed, is a bit of a limitation to the AI builder. Less normalised data is more common in Canvas apps or SharePoint linked apps, but for structured data environments with relationships and normalised data this is a limitation that will hopefully be removed as Text Classification matures.&lt;/p>
&lt;p>Also, option sets are not available, again another common categorisation technique. Multi-select option sets are an ideal tagging method too. Assume that this will come in time.&lt;/p>
&lt;p>For my PoC, I created a new field, put it on the Case form and started filling it in for a few records.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-6.png"
loading="lazy"
>&lt;/p>
&lt;p>Select the separator. If your tag field contains multiple tags, separated by a comma or semi-colon, this is where you configure it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>It also gives you a preview of what the tags the AI build would find using your chosen method. You can see in the No separator option, &amp;ldquo;printer; 3d&amp;rdquo; is one tag, rather than the assume 2 tags as displayed if semi-colon is selected. This depends on your data.&lt;/p>
&lt;p>The next page displays a review for your data and the tags that the AI builder finds.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-8.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, select a language for the text field dependent on your data.&lt;/p>
&lt;p>Once selected, train your model. This is where I started to run into problems. My initial population of tags was not enough. The training came back quickly with an error. There should be a minimum of 10 texts per tag, which I didn&amp;rsquo;t have. That would be hundreds of rows. How was I going to automate creating data to give the Text AI enough data to be a suitable demo?&lt;/p>
&lt;h2 id="getting-data">Getting Data&lt;/h2>
&lt;p>I need thousands of records to train my model properly, thousands of records relevant to the tags I create. No online data creator seemed suitable, as it wasn&amp;rsquo;t specific enough, so how? A flow.&lt;/p>
&lt;p>First I created a column in the Contact table to store a number for my contact, a unique no so I can randomise the selection of a contact.&lt;/p>
&lt;p>Next, I need some data for the case description and the tags. This is already done as it is the same as the utterances and intents I used for LUIS, so I exported the LUIS configuration, put the data in an excel file &amp;amp; added a number to that.&lt;/p>
&lt;h2 id="ready-for-the-flow">Ready for the Flow&lt;/h2>
&lt;p>My simple flow is described below.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-10.png"
loading="lazy"
>&lt;/p>
&lt;p>Ask for the number of cases to create, keep creating cases until you have reached that limit using a random contact and a random description.&lt;/p>
&lt;p>This flow is triggered manually so I start with a manual trigger and also prompt for the number of cases to create,&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-9.png"
loading="lazy"
>&lt;/p>
&lt;p>The Subject variable is used later to define the reference for the subject we have chosen.&lt;/p>
&lt;p>The default for loops is 60. I realised late on in the day that you can change that, but breaking up loops is good practice, to limit the scope of what could go wrong, so created a loop within a loop structure for my flow.&lt;/p>
&lt;p>I restrict the inner loop to 50 loops maximum, which means the number of times I run this loop has to be calculated. If I want a 920 cases created, my outer loop would occur 45 times, each creating 50 cases. I would then do a final set for the rest.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-11.png"
loading="lazy"
>&lt;/p>
&lt;p>The next steps will initialise some counters used in the loops. I also want to ensure that if the user wants to create less than 50 records, the outer loop doesn&amp;rsquo;t run at all.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-13.png"
loading="lazy"
>&lt;/p>
&lt;p>The outer loop will run for the number of loops I have calculated. This is the loop condition. The counter increments as the last thing in the outer loop. The first thing in my outer loop is to reset the case counter. This is the counter for the 0-50. If we are in this inner loop, at least 50 cases will be created.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-14.png"
loading="lazy"
>&lt;/p>
&lt;p>The first thing it does is get a random contact by using a odata filter to filter on the number field created specifically using a random number from 0-875 (875 being the highest number in that table).&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-15.png"
loading="lazy"
>&lt;/p>
&lt;p>Once the contact is retrieved, find a random description / tag combination. The data from the LUIS utterances is held in an Excel file on a Teams site. Again, a rand() function takes a random number up to the maximum in that file.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-16.png"
loading="lazy"
>&lt;/p>
&lt;p>Because more than one subject row could be returned and the fact I don&amp;rsquo;t like apply to each inside each other, set the subject Id variable.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-17.png"
loading="lazy"
>&lt;/p>
&lt;p>Ready to create a case now.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-18.png"
loading="lazy"
>&lt;/p>
&lt;p>Nothing special. It also populates the tag field.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-19.png"
loading="lazy"
>&lt;/p>
&lt;p>After some testing, to ensure that the case has the necessary fields entered, the flow was run for a thousand records without an issue.&lt;/p>
&lt;p>Creating data this way isn&amp;rsquo;t quick, 20 mins for 1000 records, but it is easy and allows you to bring in reference data quickly. Superb for PoC environments.&lt;/p>
&lt;h2 id="training-your-model-with-data">Training your Model (with data)&lt;/h2>
&lt;p>Once this data is generated, it was time to re-train my model. It ran through with success this time.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-20.png"
loading="lazy"
>&lt;/p>
&lt;p>The model is 97% sure that whatever I throw at it, it should be able to match it against the tags. There is a quick test option here too, which allows entry of a sample phrase to check your model&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-21.png"
loading="lazy"
>&lt;/p>
&lt;p>All ready to publish.&lt;/p>
&lt;h2 id="publishing-your-model">Publishing your Model&lt;/h2>
&lt;p>Publishing the model allows it be used within Flow and PowerApps.&lt;/p>
&lt;p>Clicking Publish generates a child table of the entity you first chose where the predictions are stored. The &lt;a class="link" href="https://docs.microsoft.com/en-us/ai-builder/publish-text-classification-model" target="_blank" rel="noopener"
>documentation&lt;/a> states the table will be TC_{model_name} but it created mine with gobbledegook.&lt;/p>
&lt;p>The link on the form helpfully allows you to go straight to the entity in the new customisation interface, where you can change the label of the entity.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-22.png"
loading="lazy"
>&lt;/p>
&lt;p>Also, it is useful to change some of the views, particularly the associated results view. By default it includes name &amp;amp; date, which is pretty useless, so add the tag and the probability.&lt;/p>
&lt;p>As this is a child table of Case, it is by default visible in the case form Related navigation item. In the classic customisation interface, you can change the label of this view.&lt;/p>
&lt;p>As it is published, users can use flow and the Predict action to predict the tag for a given section of text, useful if you want to do stuff before it reaches an environment.&lt;/p>
&lt;p>Now that it is published, you need to allow the model to run. This means it runs every time there is a change to the text field. This is all done via Flow, so will use your flow runs. It stores the result in the new entity.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-23.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-24.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-25.png"
loading="lazy"
>&lt;/p>
&lt;p>If a case is created now, it automatically creates the tag secondary record.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-case-creation.gif"
loading="lazy"
>&lt;/p>
&lt;h2 id="using-the-tags">Using the tags&lt;/h2>
&lt;p>As AI builder generates a record for you with its prediction, and the data is in CDS, it is a simple Flow to utilise that. As it creates a record in the AI Tags table, update the corresponding case to change the subject accordingly.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-26.png"
loading="lazy"
>&lt;/p>
&lt;p>Simple trigger when a record is created. The first action is to find the subject from the tag.&lt;/p>
&lt;p>Update the case record with the subject and the tag so the AI can be retrained later.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-27.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/09-image-28.png"
loading="lazy"
>&lt;/p>
&lt;p>That&amp;rsquo;s it. Replacing LUIS with a more user friendly environment is definitely a tick in the box for Microsoft. The AI in PowerApps feels like a simple, user friendly stepping stone for a lot of businesses into the AI world. Hopefully, businesses will embrace these simple models to leverage tools to shortcut processes, improving Employee and customer experiences.&lt;/p></description></item><item><title>Adaptive Cards - Improved Approvals (Part 2)</title><link>https://linked365.blog/2019/07/05/adaptive-cards-improved-approvals-part-2/</link><pubDate>Fri, 05 Jul 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/07/05/adaptive-cards-improved-approvals-part-2/</guid><description>&lt;img src="https://linked365.blog/images/2019/07-image-24.png" alt="Featured image of post Adaptive Cards - Improved Approvals (Part 2)" />&lt;p>Continuing on a walkthrough of creating a more effective adaptive card for approvals, this part will describe the flow I created to generate the card as well as complete the action in D365 depending on the response&lt;/p>
&lt;h2 id="objectives">Objectives&lt;/h2>
&lt;ul>
&lt;li>The Scenario (&lt;a class="link" href="https://linked365.blog/2019/07/04/adaptive-cards-improved-approvals-part-1/" >Part 1&lt;/a>)&lt;/li>
&lt;li>Preventing progress of an Opportunity ( &lt;a class="link" href="https://linked365.blog/2019/07/04/adaptive-cards-improved-approvals-part-1/" >Part 1&lt;/a> )&lt;/li>
&lt;li>Using Flow to create a basic Approval ( &lt;a class="link" href="https://linked365.blog/2019/07/04/adaptive-cards-improved-approvals-part-1/" >Part 1&lt;/a> )&lt;/li>
&lt;li>Creating an Adaptive Card ( &lt;a class="link" href="https://linked365.blog/2019/07/04/adaptive-cards-improved-approvals-part-1/" >Part 1&lt;/a> )&lt;/li>
&lt;li>Using Flow to create the Approval (This Part)&lt;/li>
&lt;li>Updating the Opportunity (This Part)&lt;/li>
&lt;/ul>
&lt;h2 id="starting-out">Starting out&lt;/h2>
&lt;p>As previously described, the Flow is triggered when a user updates the Develop Propsal checkbox. In the first stages, the flow also retrieves some records that are needed later on for population of the card. There are also initialisations of 2 arrays that are used to populate the approvers and product lines on the card.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-15.png"
loading="lazy"
>&lt;/p>
&lt;p>The next section is used to retrieve the approvers for the territory. In part 1, a many to many relationship was added, linking User to Territory via the territory approvers table.&lt;/p>
&lt;p>As the territory approvers table is a many to many relationship, it does not appear as a standard table in the common data service connector, nor the D365 connector. There are various blog posts out there which state you can just use a custom value, naming the table, but I couldn&amp;rsquo;t get it working, so I fell back to my custom connector.&lt;/p>
&lt;p>In my previous post on &lt;a class="link" href="https://linked365.blog/2019/06/10/user-admin-powerapp-part-1/" target="_blank" rel="noopener"
>Security roles via a PowerApp&lt;/a>, the custom connector which allows an FetchXML string to be sent against an object is used a lot to get the teams and the roles for a user. This connector is again used to find the users associated with a territory via the new relationship. The FetchXML is below.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;fetch&lt;/span> &lt;span class="na">top=&lt;/span>&lt;span class="s">&amp;#39;50&amp;#39;&lt;/span> &lt;span class="nt">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;entity&lt;/span> &lt;span class="na">name=&lt;/span>&lt;span class="s">&amp;#39;systemuser&amp;#39;&lt;/span> &lt;span class="nt">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;attribute&lt;/span> &lt;span class="na">name=&lt;/span>&lt;span class="s">&amp;#39;internalemailaddress&amp;#39;&lt;/span> &lt;span class="nt">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;attribute&lt;/span> &lt;span class="na">name=&lt;/span>&lt;span class="s">&amp;#39;fullname&amp;#39;&lt;/span> &lt;span class="nt">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;link-entity&lt;/span> &lt;span class="na">name=&lt;/span>&lt;span class="s">&amp;#39;cc\_territory\_approver&amp;#39;&lt;/span> &lt;span class="na">from=&lt;/span>&lt;span class="s">&amp;#39;systemuserid&amp;#39;&lt;/span> &lt;span class="na">to=&lt;/span>&lt;span class="s">&amp;#39;systemuserid&amp;#39;&lt;/span> &lt;span class="na">intersect=&lt;/span>&lt;span class="s">&amp;#39;true&amp;#39;&lt;/span> &lt;span class="nt">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;filter&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;condition&lt;/span> &lt;span class="na">attribute=&lt;/span>&lt;span class="s">&amp;#39;territoryid&amp;#39;&lt;/span> &lt;span class="na">operator=&lt;/span>&lt;span class="s">&amp;#39;eq&amp;#39;&lt;/span> &lt;span class="na">value=&lt;/span>&lt;span class="s">&amp;#39;@{body(&amp;#39;&lt;/span>&lt;span class="err">Get\_Account\_Manager&amp;#39;)?\[&amp;#39;\_territoryid\_value&amp;#39;\]}&amp;#39;&lt;/span> &lt;span class="nt">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;/filter&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;/link-entity&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;/entity&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/fetch&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p> This will return JSON which corresponds to the users linked as approvers to the territory.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;@odata.etag&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;W/\\&amp;#34;&lt;/span>&lt;span class="mi">3421832&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;internalemailaddress&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;veronicaq@CRM568082.OnMicrosoft.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;fullname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Veronica Quek&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;systemuserid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;824da0b2-6c88-e911-a83e-000d3a323d10&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ownerid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;824da0b2-6c88-e911-a83e-000d3a323d10&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;@odata.etag&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;W/\\&amp;#34;&lt;/span>&lt;span class="mi">1742271&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;internalemailaddress&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;danj@CRM568082.OnMicrosoft.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;fullname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Dan Jump&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;systemuserid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;e3b305bf-6c88-e911-a83e-000d3a323d10&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ownerid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;e3b305bf-6c88-e911-a83e-000d3a323d10&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;@odata.etag&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;W/\\&amp;#34;&lt;/span>&lt;span class="mi">3422353&lt;/span>&lt;span class="err">\\&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;internalemailaddress&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;CarlC@CRM568082.onmicrosoft.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;fullname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Carl Cookson&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;systemuserid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;113f1e3a-db90-e911-a822-000d3a34e879&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ownerid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;113f1e3a-db90-e911-a822-000d3a34e879&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">\&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p> An approval needs a list of email addresses, separated with a ; . To achieve this, firstly put each of the returned email addresses in an array, then use the Join function to create the string used for approvers&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-17.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="populated-the-main-approval">Populated the Main approval&lt;/h2>
&lt;p>The next part the body of the approval that is going to be sent. I&amp;rsquo;ll link the full version of this at the end of the article, but effectively, you copy your design, remembering to insert appropriate dynamic content on the way.&lt;/p>
&lt;p>Here, I create the 2 URLs that are displayed in the card, which combine the starting point of url and append Account or Opportunity Id.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-18.png"
loading="lazy"
>&lt;/p>
&lt;p>This is displayed at the top of the card.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-19.png"
loading="lazy"
>&lt;/p>
&lt;p>Further, formatting currencies is difficult in Flow. (I stand to be corrected). I found this &lt;a class="link" href="https://powerusers.microsoft.com/t5/Using-Flows/Format-number-with-thousands-separator-in-Flow-email/m-p/305263/highlight/true#M7484" target="_blank" rel="noopener"
>post&lt;/a> on Power Platform community which highlights the issue and degvalentine has the solution, which I have tweaked to take into account of null values in the fields in D365. This example is for one of the fields on the secondary grid.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">empty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">])),&lt;/span> &lt;span class="sc">&amp;#39;0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nf">concat&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">greaterOrEquals&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">1000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">concat&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">substring&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">sub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">length&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">first&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">))),&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sc">&amp;#39;,&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">substring&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">first&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">sub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">length&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">first&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">))),&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">min&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">length&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">first&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">))))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">first&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">concat&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">last&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">less&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">length&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">last&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">manualdiscountamount&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">]),&lt;/span> &lt;span class="sc">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">))),&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sc">&amp;#39;0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">&amp;#39;&lt;/span>&lt;span class="mo">00&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="populating-product-details">Populating Product details&lt;/h2>
&lt;p>As the approval body is built up, the next stage is to create a table with the product lines in it. Getting the lines is a simple filter query using the primary key on Opportunity.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-20.png"
loading="lazy"
>&lt;/p>
&lt;p>Like with the approvers, an array is populated with a formatted version of each line, taking fields returned and combining them with formatting rules.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-22.png"
loading="lazy"
>&lt;/p>
&lt;p>The first expression deals with the fact one of the products chosen to demo had a double quote (&amp;quot;) in it, which messes up JSON if it isn&amp;rsquo;t escaped as it is the string delimiter. I used a simple replace expression to add a &amp;ldquo;\&amp;rdquo; before it.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="nf">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">items&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">Add&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_to&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_Prod&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="n">_LInes&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="n">productname&lt;/span>&lt;span class="err">&amp;#39;\&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="sc">&amp;#39;&amp;#34;&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">&amp;#39;\\&lt;/span>&lt;span class="s">&amp;#34;&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The next expression is the one above to format the currency with the appropriate commas and decimal places.&lt;/p>
&lt;p>The output of this looping of the product lines is then combined using a join again, then combined with the body main string.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-23.png"
loading="lazy"
>&lt;/p>
&lt;p>The bottom of this string starts the list of actions, which are the buttons.&lt;/p>
&lt;p>The next step is to create the Approval. This is pretty simple, using a first to respond type, and fleshing it out a bit, so if a user uses the standard Flow Approval interface, they have something to relate to. No notification is needed, this will send an email to the approver, but the Flow will alert the approver via Teams.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-24.png"
loading="lazy"
>&lt;/p>
&lt;p>My original design for this PoC was to push this notification / approval to a Team channel, one notice to the Approvers channel. As Teams integrates with D365, it did not seem much of a hop to highlight the Opportunity approval.&lt;/p>
&lt;p>The only issue is that approvals don&amp;rsquo;t work in team channels, only when sent to a user. Until this is resolved by MS, you are limited to sending the approval to an individual in Teams.&lt;/p>
&lt;h2 id="sending-the-approval">Sending the Approval&lt;/h2>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-25.png"
loading="lazy"
>&lt;/p>
&lt;p>The key bits of this action is ensuring you have the Approvers tenant (can you post approvals across tenants?), the respond link, the approval ID and the creation time populated with the data coming from the approval. The same goes for the Reject action.&lt;/p>
&lt;p>That&amp;rsquo;s it, the new approval is sent.&lt;/p>
&lt;h2 id="waiting-for-the-approval">Waiting for the Approval&lt;/h2>
&lt;p>As the approval is configured that anyone could approve or reject, the next action is to wait for that approval to happen. Approvals can happen upto 30 days, which is another issue, but as this is to speed up the approval process, let&amp;rsquo;s not worry about that.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-26.png"
loading="lazy"
>&lt;/p>
&lt;p>If the outcome is approved, then the field Complete Internal Review is checked and a note is created, linked to the Opportunity logging who approved it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-27.png"
loading="lazy"
>&lt;/p>
&lt;p>This is in a loop, as, in theory, there could be more than one approver on an approval, if you use the Approval setting that forces everyone to approve something.&lt;/p>
&lt;p>The Regarding / Regarding type, highlighted above, need to be populated as you get orphan records and can spend 20 minutes wondering what is wrong (not me obviously)&lt;/p>
&lt;p>On the Reject side of the condition, the Opportunity is put back to the state it was in before the flow started, namely Develop Proposal is reset. This triggers our Flow again, but as long as the first condition is met, it won&amp;rsquo;t go any further. A note is also added, to highlight who rejected it and why.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-28.png"
loading="lazy"
>&lt;/p></description></item><item><title>Adaptive Cards - Improved Approvals (Part 1)</title><link>https://linked365.blog/2019/07/04/adaptive-cards-improved-approvals-part-1/</link><pubDate>Thu, 04 Jul 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/07/04/adaptive-cards-improved-approvals-part-1/</guid><description>&lt;img src="https://linked365.blog/images/2019/07-image-12.png" alt="Featured image of post Adaptive Cards - Improved Approvals (Part 1)" />&lt;p>&lt;a class="link" href="https://adaptivecards.io/" target="_blank" rel="noopener"
>Adaptive cards&lt;/a> are relatively new to the stack of tools available to PowerPlatform users, emerging from Message Cards. They are a great way of interacting with users who are not a typical D365 user, those on the periphery who are interested in the data but not the detail.&lt;/p>
&lt;h2 id="objectives">Objectives&lt;/h2>
&lt;ul>
&lt;li>The Scenario (This Part)&lt;/li>
&lt;li>Preventing progress of an Opportunity (This Part)&lt;/li>
&lt;li>Using Flow to create a basic Approval (This Part)&lt;/li>
&lt;li>Creating an Adaptive Card (This Part)&lt;/li>
&lt;li>Using Flow to create the Approval&lt;/li>
&lt;li>Updating the Opportunity&lt;/li>
&lt;/ul>
&lt;h2 id="the-scenario">The Scenario&lt;/h2>
&lt;p>Big Energy is going well, they are now involved in some big deals for big enterprises which need a lot of time to land. The proposals that are generated are complicated, and they struggled with some dubious sales people reducing the margins just to get the deals and this is just bad for business.&lt;/p>
&lt;p>An approval process needs to be implemented, where one or more of a designated group of individuals per territory review the opportunity and decide if the margins are appropriate.&lt;/p>
&lt;p>Unfortunately, the approvers tend to be very busy senior directors, who use D365 sporadically, if at all, and Big Energy need to allow them to approve the opportunities where ever they are using Outlook or Teams as the preferred option.&lt;/p>
&lt;h2 id="tweaking-the-standard-sales-process">Tweaking the standard Sales process&lt;/h2>
&lt;p>Microsoft provides a Business Process Flow for Opportunity management, and in our scenario, only the approvers should be able to check the boolean Complete Internal Review. This is part of the standard Propose stage of the BPF.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-3.png"
loading="lazy"
>&lt;/p>
&lt;p>To &amp;ldquo;lock&amp;rdquo; (I know it isn&amp;rsquo;t foolproof, what is?) the progress on Propose, the Complete Internal Review is subject to a simple business rule, if the opportunity is at any stage, lock the field.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-1.png"
loading="lazy"
>&lt;/p>
&lt;p>Now, no one can edit that field, if that field is made mandatory to progress the bpf stage, no one can progress the stage past propose.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-2.png"
loading="lazy"
>&lt;/p>
&lt;p>Territories are often used in Sales to group accounts or account managers and in our scenario, there is a set list of approvers for a territory. I have added a new many - to - many relationship for this, Approvers and ensured it is listed in the user form as one of the relationships&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-16.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="using-flow-to-create-an-approval">Using Flow to create an Approval&lt;/h2>
&lt;p>In the standard Propose stage, there is another boolean that is of interest, Develop Proposal. The Flow is triggered when this value is changed. A simple CDS update trigger is the starting point.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-4.png"
loading="lazy"
>&lt;/p>
&lt;p>The next stage is to confirm that this trigger is coming with the correct record state, the record has been marked with Develop Proposal, but the other field, Complete Internal Review is still empty.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-5.png"
loading="lazy"
>&lt;/p>
&lt;p>The flow to create the adapted card is fairly intense, well, from my experience, as you will see, so for now, create an Approval using enough details to get the default experience that can be built on.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>In Details, there is a lot you can do, using &lt;a class="link" href="https://docs.microsoft.com/en-us/flow/approvals-markdown-support" target="_blank" rel="noopener"
>markdown&lt;/a> but this is not as comprehensive as the formating you get from adaptive cards.&lt;/p>
&lt;p>When this flow is run, you will get an email to the assigned to with a simple, standard approval, which is in itself, an adaptive card, but it is fairly plain.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-8.png"
loading="lazy"
>&lt;/p>
&lt;p>Using the Flow history, this action also shows the adaptive card that was built&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-9.png"
loading="lazy"
>&lt;/p>
&lt;p>Copying this value into the Adaptive card designer JSON section gives the format for a basic design, which can be augmented to show some proper information&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-10.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="building-an-adaptive-card">Building an Adaptive card&lt;/h2>
&lt;p>Adaptive Cards are a means to interact with your users via email, teams or any other app that handles the rendering of them. They have actions, allow images to be presented and can format text in a markup that imitates a comprehensive website. They are supported in Outlook mobile apps as well as O365, either using the main app or online.&lt;/p>
&lt;p>They work by rendering a JSON object, which can be formatted to match the host application (the dark black Teams theme for example renders it very differently, but the core actions are still there.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-11.png"
loading="lazy"
>&lt;/p>
&lt;p>Microsoft has built a superb tool to manage Adaptive cards, the new version, at adaptivecards.io/designer. This site has lots of examples to get you started, the &lt;a class="link" href="https://adaptivecards.io/samples/ExpenseReport.html" target="_blank" rel="noopener"
>Expense Report&lt;/a> is a good starting point from a design point of view, but the standard approval card forms the base for the card. There are bits in it that you need to incorporate into your card to allow the approval to work.&lt;/p>
&lt;p>The parts in the data section are the essential bits that, in our adopted JSON need to be duplicated or populated by Flow to allow our card to act as an approval.&lt;/p>
&lt;p>My card is a bit different than the standard, displaying key parts of the Opportunity and the associated product lines.&lt;/p>
&lt;p>&lt;img src="https://linkd365home.files.wordpress.com/2019/07/image-12.png?w=457"
loading="lazy"
>&lt;/p>
&lt;p>As you can see, there is a lot more information on what is happening on the opportunity, probably enough for a sales manager to make a decision in most cases. Included in the card are links to the Account and Opportunity if further review is needed.&lt;/p>
&lt;p>I would recommend starting from a sample and building your content, with dummy data, so you can get the layout correct.&lt;/p>
&lt;p>Each of the buttons are also cards on their own, allowing a comment to be made before the approval is approved or rejected.&lt;/p>
&lt;p>These have been copied from the standard adaptive card produced by the Flow approval so that the submitted approval works like a standard approval.&lt;/p>
&lt;h2 id="some-considerations-and-limitations">Some considerations and limitations&lt;/h2>
&lt;p>I first started trying to reproduce the Expense Approval card in full from the samples&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/07-image-13.png"
loading="lazy"
>&lt;/p>
&lt;p>This has a great use of hidden / visible sections of the expense lines which could give you a lot of real estate for Opportunity lines. Unfortunately, these are not rendered in Teams.&lt;/p>
&lt;p>Also, I thought I would be able to use HTTP trigger, but again, any button with an HTTP trigger is ignored in teams, you are only allowed to create actions for opening URLs, submitting, hiding parts and showing a secondary card.&lt;/p>
&lt;p>Below the main part of the designer is the JSON, which is created by any changes you make above but also can be edited and reflected in the visualiser. The snippet below is taken from the standard card, which contains all the bits that need duplicating to ensure the new, improved approval works correctly.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;actions&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Action.ShowCard&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Approve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;card&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;AdaptiveCard&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;body&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;TextBlock&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Comments&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wrap&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Input.Text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;comments&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;placeholder&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Enter comments&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;maxLength&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1000&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;isMultiline&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;actions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Action.Submit&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Submit&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Environment&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Default-2821cf92-86ad-4c7b-ba9a-5c79a70d4a21&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ApprovalTitle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Appoval required for Opportunity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ApprovalLink&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://flow.microsoft.com/manage/environments/Default-2821cf92-86ad-4c7b-ba9a-5c79a70d4a21/approvals/received/6cce94f6-603c-40e7-adb6-8b20c75f724f&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ApprovalName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;6cce94f6-603c-40e7-adb6-8b20c75f724f&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ItemLink&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://.crm.dynamics.com/main.aspx?newWindow=true&amp;amp;pagetype=entityrecord&amp;amp;etn=opportunity&amp;amp;id=b7c47c42-a290-e611-80e3-c4346bacba3c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ItemLinkDescription&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Opportunity for 7-Eleven and Udaside label - &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;OnBehalfOfNotice&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Requested by Carl Cookson &amp;lt;CarlC@CRM.onmicrosoft.com&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;CreatorName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Carl Cookson&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;CreatorEmail&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;CarlC@CRM.onmicrosoft.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;CreationTime&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;\\&amp;#34;&lt;/span>&lt;span class="mi">2019-07-03&lt;/span>&lt;span class="err">T&lt;/span>&lt;span class="mi">14&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">30&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">02&lt;/span>&lt;span class="err">Z\\&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;MessageTitle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Appoval required for Opportunity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Options&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Approve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Reject&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;SelectedOption&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Approve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ActionType&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;$schema&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://adaptivecards.io/schemas/adaptive-card.json&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div></description></item><item><title>User Admin PowerApp (Part 1)</title><link>https://linked365.blog/2019/06/10/user-admin-powerapp-part-1/</link><pubDate>Mon, 10 Jun 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/06/10/user-admin-powerapp-part-1/</guid><description>&lt;img src="https://linked365.blog/images/2019/06-image-3.png" alt="Featured image of post User Admin PowerApp (Part 1)" />&lt;p>Sorry it has been a while since my last blog post, this scenario has taken a while to get it to the state where I was happy to show it off. Mainly due to my own lack of understanding of the intricacies of the D365 API, but also been busy external to the blog, you know real life.&lt;/p>
&lt;h2 id="objectives">Objectives&lt;/h2>
&lt;ul>
&lt;li>The Scenario (This part)&lt;/li>
&lt;li>Notifying the manager of a new Employee (This Part)&lt;/li>
&lt;li>PowerApp to display and update User Data&lt;/li>
&lt;li>Update Roles and Teams&lt;/li>
&lt;/ul>
&lt;h2 id="the-scenario">The Scenario&lt;/h2>
&lt;p>Big Energy Co is going from strength to strength, presumably because of the innovative solutions using &lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365-part-1/" >LUIS&lt;/a>, &lt;a class="link" href="https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/" >Alexa&lt;/a> and &lt;a class="link" href="https://linked365.blog/2019/05/04/ifttt-flow-d365/" >IFTTT&lt;/a>.&lt;/p>
&lt;p>The HR department is ramping up recruitment and new teams are being shaped to support all the growth.&lt;/p>
&lt;p>One of the criticisms from the managers is that it takes a while for the IT / D365 administrators to get users in the correct teams and security roles so they can be effective in D365.&lt;/p>
&lt;p>A clever chap in the management team suggested that they be given an app that would allow a manager to update the roles and teams (and other relevant parts of a user) without resorting to logging into D365 administration. Something they can use wherever they have WiFi or a data connection.&lt;/p>
&lt;p>It would also be good to get a notification when they have a new employee, or someone is added to their reports.&lt;/p>
&lt;h2 id="the-flow">The Flow&lt;/h2>
&lt;p>This flow is quite simple, trigger an email when a user has the Manager field (parentsystemuserid) field updated. O365 will create the user for us (assuming you are in the cloud) and an administrator will still have to update the users manager.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/06-image.png"
loading="lazy"
>&lt;/p>
&lt;p>Here, the attribute I am interested on is parentsystemuserid.&lt;/p>
&lt;p>Next, just check to see if the manager is actually populated. In a lot of businesses, removing their manager is part of the process on off-boarding an employee, to tidy up selection lists etc.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/06-image-1.png"
loading="lazy"
>&lt;/p>
&lt;p>Then, get the manager user record from D365 so that the email can be sent to it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/06-image-2.png"
loading="lazy"
>&lt;/p>
&lt;p>Told you it was simple. I am sure that this can have more logic - Do we need an approval step before assigning this user? Do we have to wait for HR to do some work and only activate the user once all the checks are done?&lt;/p>
&lt;p>Next, I&amp;rsquo;ll step through the PowerApps set up to retrieve data from my reports.&lt;/p></description></item><item><title>Creating Attachments in D365</title><link>https://linked365.blog/2019/05/11/creating-attachments-in-d365/</link><pubDate>Sat, 11 May 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/05/11/creating-attachments-in-d365/</guid><description>&lt;img src="https://linked365.blog/images/2019/05-squirrel-eating.jpg" alt="Featured image of post Creating Attachments in D365" />&lt;p>This isn&amp;rsquo;t one of my usual posts as I always like to start with a business case. I intend to create a blog about the business case / solution that created this issue once all the other  pieces fall into place, so watch out for that (keep them keen by teasing they say)&lt;/p>
&lt;h2 id="the-problem">The Problem&lt;/h2>
&lt;p>You might not know there is a problem, but there is. Squirrels are a problem, but not my field of expertise, &lt;a class="link" href="https://twitter.com/dynamiccrmcat" target="_blank" rel="noopener"
>@dynamiccrmcat&lt;/a> will have other opinions.&lt;/p>
&lt;p>Attachments are a problem. More specifically, trying to add an attachment to D365 via a Flow or PowerApp is a problem. Yes I know we should be using SharePoint or Teams etc, but sometimes you want to keep your data in D365.&lt;/p>
&lt;p>You should just be able to &amp;ldquo;Patch&amp;rdquo; the Notes entity in a PowerApp with the file you want in the documentbody field of the entity, but you run into problems (bug) with objecttypecodes (it is expecting a GUID and all you know is an entity name).&lt;/p>
&lt;p>&lt;a class="link" href="https://twitter.com/jukkan" target="_blank" rel="noopener"
>@JukkaN&lt;/a> pointed me to this &lt;a class="link" href="https://powerusers.microsoft.com/t5/General-Discussion/Not-able-to-create-the-attachment-regarding-any-entity/td-p/193465" target="_blank" rel="noopener"
>article&lt;/a> on the PowerApp forum which explains the problem in more detail and clearer than I have. This was via a conversation with him and &lt;a class="link" href="https://twitter.com/TattooedCRMGuy" target="_blank" rel="noopener"
>@TattooedCRMGuy&lt;/a> where we came to the conclusion that there is a bug in the CDS connector.&lt;/p>
&lt;p>So my next thought was to call a Flow from the PowerApp, passing in the file as a parameter. This doesn&amp;rsquo;t work either. Any which way you try, it ends up with the file being passed as the link to the Azure blob the PowerApp is temporarily using to store the attachment.&lt;/p>
&lt;p>Then I started using the developers friend, Google. My first thought is that if I could pass to the Flow a file rather than the URL, this would work. This thought led me to an excellent Youtube &lt;a class="link" href="https://www.youtube.com/watch?v=mp-8B1fLrqs&amp;amp;feature=youtu.be" target="_blank" rel="noopener"
>video&lt;/a> by Paul Culmsee. He shows how to pass a Photo from PowerApps to Flow to save it to Sharepoint. He has the same issue, how to pass a file to flow, and thanks to him, I have duplicated it for saving a file to D365.&lt;/p>
&lt;p>So the logic I have deployed is&lt;/p>
&lt;p>PowerApps → Custom Connector → Flow → Custom Connector into D365&lt;/p>
&lt;p>2 custom connectors here, this is a relative expensive solution, as the user require a PowerApps Plan 1 license for this which might be an addon to their license.&lt;/p>
&lt;h2 id="the-d365-connector">The D365 Connector&lt;/h2>
&lt;p>Starting at the final step, I utilised a custom connector like in my &lt;a class="link" href="https://linked365.blog/2019/05/04/ifttt-flow-d365/" >previous&lt;/a> &lt;a class="link" href="https://linked365.blog/2019/04/03/connecting-luis-d365-part-4-custom-connector/" >posts&lt;/a> to interact with the D365 API directly. I am not going to go through the method of creating the connector, just this custom action. Again, PostMan is your friend. Using this tool, I generated a JSON template to post against the Annotations entity. Firstly, add an action to the connector. &lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-8.png"
loading="lazy"
>&lt;/p>
&lt;p>Next, select Import from sample, and populate as below, obviously using your own instance API endpoint.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-9.png"
loading="lazy"
>&lt;/p>
&lt;p>Couple of things here, Annotation is the name of the table that Notes are stored in. &amp;ldquo;objectid_contact@odata.bind&amp;rdquo; is the single value navigation for linking Contact to the note. In the CDS connector, this field is not visible, you are expected to enter a pair of _objectid_value and objecttypecode, which doesn&amp;rsquo;t work, hence this blog post (I hope they fix it, but not too soon now I have worked it out). Finally, documentbody is the field where the attachment is stored, as a Base64 string. This is weirdly different than the binary data store used by Sharepoint.&lt;/p>
&lt;p>Select Import and the first connector is done.&lt;/p>
&lt;h2 id="the-flow">The Flow&lt;/h2>
&lt;p>The flow is pretty simple, a HTTP trigger, messing around with the inputs and sending the information to the D365 connector. Simple, but I needed the video by Paul Culmsee to guide me through. The premise being, rather than the usual approach of looking at the body of the web call, we need to take out parts from the query string and the content of the call would be a file. He does a much better job at explaining it that I do, so head over to the &lt;a class="link" href="https://www.youtube.com/watch?v=mp-8B1fLrqs&amp;amp;feature=youtu.be" target="_blank" rel="noopener"
>video&lt;/a> to actually learn.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-10.png"
loading="lazy"
>&lt;/p>
&lt;p>A standard HTTP trigger. I then use a Compose data operation to take data from the trigger using a formula based on the query parameters passed.&lt;/p>
&lt;p>trigger()[&amp;lsquo;outputs&amp;rsquo;][&amp;lsquo;queries&amp;rsquo;][&amp;lsquo;filename&amp;rsquo;]&lt;/p>
&lt;p>This states that I want find and store the filename parameter passed in the url when the trigger was triggered&lt;/p>
&lt;p>The Get File Body does the same but looks at the content of the body that was passed in.&lt;/p>
&lt;p>triggerMultipartBody(0)[&amp;rsquo;$content&amp;rsquo;]&lt;/p>
&lt;p>The final part is a call to the D365 custom connector, passing in the compose operation outputs&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-11.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="another-custom-connector">Another Custom Connector&lt;/h2>
&lt;p>You can&amp;rsquo;t call a webservice triggered flow from a PowerApp directly. You can call a Flow, but the flow doesn&amp;rsquo;t pass the appropriate parameters. It only deals with strings. You can&amp;rsquo;t convert your file to a string in PowerApps. I am obviously going to be proved wrong here, but I will learn. That&amp;rsquo;s one of the reasons I blog.&lt;/p>
&lt;p>Hence, why you need to create a custom connector to pass from PowerApps to the Flow above.&lt;/p>
&lt;p>Connectors can be created by uploading a swagger definition. &lt;a class="link" href="https://en.wikipedia.org/wiki/Swagger_%28software%29" target="_blank" rel="noopener"
>Swagger&lt;/a> is an open-source framework to document APIs and has been recently converted to the OpenAPI specification. Obviously connectors can be built from scratch, but because of the file upload that is required, a definition of the API is required.&lt;/p>
&lt;p>Paul again comes to my rescue, he has a great &lt;a class="link" href="http://www.cleverworkarounds.com/2017/11/13/a-sample-openapiswagger-file-for-powerapps/" target="_blank" rel="noopener"
>blog&lt;/a> post that he goes through in detail the file that was produced to support his video. In the Youtube post, he uses a tool that I can not find, but this file walkthrough was enough for me to produce my own &lt;a class="link" href="https://1drv.ms/u/s!AueNWhtMOmpUiqszYVB8owfI-DLujA" target="_blank" rel="noopener"
>file&lt;/a>. I am not going to go through the detail here, Paul does a much better job.&lt;/p>
&lt;p>In custom connectors, hit the +, then select Import an OpenAPI file.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-12.png"
loading="lazy"
>&lt;/p>
&lt;p>Give your new connector a name and select your newly created file definition. If your file is correct, you are now presented with a pre-populated definition of your connector. The query parameters displayed include lots of configuration items that should be pre-populated from your Swagger file.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-14.png"
loading="lazy"
>&lt;/p>
&lt;p>You can see the connector is not expecting a Body.&lt;/p>
&lt;p>If you test the connector action there is also an extra parameter it is expecting&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-15.png"
loading="lazy"
>&lt;/p>
&lt;p>This File parameter is essential. Now it&amp;rsquo;s reading for use!&lt;/p>
&lt;h2 id="the-powerapp">The PowerApp&lt;/h2>
&lt;p>To demonstrate the connector, create a new PowerApp. Because this is for the contact entity, use a drop down to get a list of Contacts to attach the note to from D365.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-16.png"
loading="lazy"
>&lt;/p>
&lt;p>Associate this with D365 using the Common Data Service&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-17.png"
loading="lazy"
>&lt;/p>
&lt;p>Select the Contacts Entity, back in the properties of the control, use Full Name as the Value. Next, add a &amp;ldquo;Add Picture&amp;rdquo; control.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-18.png"
loading="lazy"
>&lt;/p>
&lt;p>Also add a datatable. This time connect to the Annotation (Notes) entity in D365. Make sure you use the D365 connector though. For some reason, the Common Data Service connector does not return the Regarding as a field you can use. Select a few relevant fields, Title, Note, File Name and Document.&lt;/p>
&lt;p>In the Values field, filter the datatable by the Contact that is selected in the dropdown and only show those where there is an attachment.&lt;/p>
&lt;p>Filter(Notes, Regarding = GUID(ContactDD.Selected.Contact) &amp;amp;&amp;amp; !IsBlank(Document))&lt;/p>
&lt;p>Selecting a contact now should deliver a list of notes that are attached to that record.&lt;/p>
&lt;p>To Upload a document, add a button. This will trigger the custom connector, so this needs to be added to the PowerApp. Select Data sources, add data source&lt;/p>
&lt;p>The custom connector that was created earlier should appear. Select it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-19.png"
loading="lazy"
>&lt;/p>
&lt;p>In the button OnSelect action, if all is well, enter the name of the connector and action and it should give you a list of parameters you need.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-20.png"
loading="lazy"
>&lt;/p>
&lt;p>The final call to the connector looks like this&lt;/p>
&lt;p>FileUploader.UploadFile(
AddMediaButton1.FileName,
ContactDD.Selected.Contact,
&amp;ldquo;Added from PowerApps&amp;rdquo;,
&amp;ldquo;Added from PowerApps&amp;rdquo;,
UploadedImage1.Image
);
Refresh(Notes)&lt;/p>
&lt;p>The filename comes from the control within the Image upload control, the contact Id is from the selected contact, some text stuff to fill out (you could add a text control to take that input obviously) and then the image.&lt;/p>
&lt;p>I refresh the notes data set after I am done so that the list has got the data.&lt;/p>
&lt;p>So that&amp;rsquo;s it, a complicated solution to a &amp;ldquo;bug/feature&amp;rdquo; currently in Powerapps.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-21.png"
loading="lazy"
>&lt;/p>
&lt;p>A screenshot of that squirrel note against the contact in D365, just so you know I am not bluffing.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-22.png"
loading="lazy"
>&lt;/p>
&lt;p>BTW, squirrels are evil, rats with marketing, don&amp;rsquo;t believe everything &lt;a class="link" href="https://twitter.com/dynamiccrmcat" target="_blank" rel="noopener"
>@dynamiccrmcat&lt;/a> says, though it is probably just squirrels she is wrong about.&lt;/p></description></item><item><title>IFTTT Flow D365</title><link>https://linked365.blog/2019/05/04/ifttt-flow-d365/</link><pubDate>Sat, 04 May 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/05/04/ifttt-flow-d365/</guid><description>&lt;img src="https://linked365.blog/images/2019/05-image-67.png" alt="Featured image of post IFTTT Flow D365" />&lt;p>This will be a quick post, as the connector doesn&amp;rsquo;t take a lot of configuration.&lt;/p>
&lt;h2 id="the-business-scenario">The Business Scenario&lt;/h2>
&lt;p>Big Energy has a lot of sales people, all over the country, which make a lot of calls, usually on their mobiles, to potential or current customers, arranging opportunities and resolving any issues.&lt;/p>
&lt;p>The Sales Director at Big Energy is concerned about the number of calls that the sales people don&amp;rsquo;t always log those calls, only when they deem it important and have no traceability about how many calls an individual has made.&lt;/p>
&lt;p>Could a solution be found that would log every call a sales person makes to a number D365 knows about?&lt;/p>
&lt;h2 id="ifttt">IFTTT&lt;/h2>
&lt;p>&lt;a class="link" href="https://ifttt.com" target="_blank" rel="noopener"
>IFTTT&lt;/a> (IF This Then That) is a free web service that allows users to create applets to connect their devices with their services. There are lots of sample applets to automate tasks, my favourite is linking Alexa&amp;rsquo;s shopping list with Tesco to add everything I add to Alexa to my Tesco order. Simples.&lt;/p>
&lt;p>IFTTT works with a lot of devices and it has a stand alone app for smart phones, allowing interaction with the device.&lt;/p>
&lt;h3 id="getting-started">Getting Started&lt;/h3>
&lt;p>Log in to IFTTT and go to My Applets, then New Applet. Hit the big blue this&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-64.png"
loading="lazy"
>&lt;/p>
&lt;p>Search for phone, and select Android Phone Call (sorry think this is only Android users)&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-65.png"
loading="lazy"
>&lt;/p>
&lt;p>This presents you with several triggers, the IF part. Select Any Phone Call placed. You will have to make another App for received, but same logic.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-66.png"
loading="lazy"
>&lt;/p>
&lt;p>The next step is to tell IFTTT what you want to do, select the big That button&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-67.png"
loading="lazy"
>&lt;/p>
&lt;p>IFTTT lets you search for all the available services that you can trigger from your data. Search for Web and select Webhooks.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-68.png"
loading="lazy"
>&lt;/p>
&lt;p>The only option here is to make a web request.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-69.png"
loading="lazy"
>&lt;/p>
&lt;p>If you have read my previous articles (if not go &lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365-part-1/" >here&lt;/a> or &lt;a class="link" href="https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/" >here&lt;/a> now and shame on you), creating a Flow trigger from a web request should be straight forward.&lt;/p>
&lt;h2 id="create-the-flow">Create the Flow&lt;/h2>
&lt;p>As in the previous articles, start with a generic http trigger in Flow. Firstly, select HTTP trigger, and enter a default body for the JSON. This is generic enough to allow the call so the schema passed from IFTTT can be created.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-70.png"
loading="lazy"
>&lt;/p>
&lt;p>As the action, send yourself an email, with the Body of the email being the body of the request.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-71.png"
loading="lazy"
>&lt;/p>
&lt;p>Hit save and go back to the trigger. This URL is the bit you need to pop back into IFTTT.&lt;/p>
&lt;h2 id="connecting-flow-and-ifttt">Connecting Flow and IFTTT&lt;/h2>
&lt;p>Back in the IFTTT Web Request, paste in the URL. I have changed the Method to Post, content type to json and included in the Body 3 ingredients (data returned by the call placed method trigger into the body. This is formatted appropriately to become a valid JSON Request&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-72.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-74.png"
loading="lazy"
>&lt;/p>
&lt;p>This is now ready for testing.&lt;/p>
&lt;h2 id="testing">Testing&lt;/h2>
&lt;p>For testing to commence, a call needs to be logged. This means you need to install the IFTTT app on your phone, log in and check the app you created is available.&lt;/p>
&lt;p>Once this is done, make a call.&lt;/p>
&lt;p>If successfully, your Flow should run, sending you an email. If not, you can look in the IFTTT log by checking the activity log.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-75.png"
loading="lazy"
>&lt;/p>
&lt;p>The log highlights what happened for each run, when it was updated etc. Not as user friendly as Flow, but at least you get an error code. I found I was getting 400 errors, as the format for the JSON wasn&amp;rsquo;t correct.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-77.png"
loading="lazy"
>&lt;/p>
&lt;p>My email also has the information sent from the request, this is used to tell Flow properly what is expected, allowing access to the properties.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-79.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="is-the-contact-known">Is the Contact known?&lt;/h2>
&lt;p>The assumption is that not all calls made by the user will be to known contacts. Either unknown to D365 or personal calls. The first thing that is required is to find the contact. Using the List Records component, use a filter query to return all contacts that match the data coming from IFTTT with any of the phone numbers held against contact. Be careful here, as the number as entered by the user in the contact on their phone or as dialed is used here.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-80.png"
loading="lazy"
>&lt;/p>
&lt;p>The next step is to check if any contacts were found. As previously, check the length of the return from the previous step has one or more contacts in it. Add the contact id to a variable if contacts were returned, terminate with grace if not.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-83.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="create-the-phone-call">Create the Phone Call&lt;/h2>
&lt;p>Creating the phone call is a straight forward call to the CDS Create Record action.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image.png"
loading="lazy"
>&lt;/p>
&lt;p>Couple of formulas here, firstly the timestamp that comes from IFTTT looks like&lt;/p>
&lt;p>May 01, 2019 at 05:50PM&lt;/p>
&lt;p>Flow doesn&amp;rsquo;t like this, so the expression for Due is&lt;/p>
&lt;p>replace(triggerBody()?[&amp;lsquo;occuredat&amp;rsquo;], &amp;rsquo; at &amp;lsquo;,&amp;rsquo; &amp;lsquo;)&lt;/p>
&lt;p>Duration from IFTTT is in seconds, in D365 is minutes. A simple divide by 60 puts in the right value&lt;/p>
&lt;p>div(int(triggerBody()?[&amp;lsquo;callLength&amp;rsquo;]),60)&lt;/p>
&lt;p>Flow is complete.&lt;/p>
&lt;h2 id="can-it-be-be-recorded-better">Can it be be recorded better?&lt;/h2>
&lt;p>The call that Flow creates is missing 2 key components, the From and To&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-1.png"
loading="lazy"
>&lt;/p>
&lt;p>The CDS connector doesn&amp;rsquo;t support &lt;a class="link" href="https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/activity-entities" target="_blank" rel="noopener"
>activity parties&lt;/a> (these fields are both party fields, the user can type and search for multiple contacts, users, leads etc to populate this normally). You can get at these via standard API calls, so back to the custom connector.&lt;/p>
&lt;p>My previous &lt;a class="link" href="https://linked365.blog/2019/04/03/connecting-luis-d365-part-4-custom-connector/" >post&lt;/a> on LUIS used a custom connector to close the incident using an action. This walks through creating the connector, so I won&amp;rsquo;t repeat myself.&lt;/p>
&lt;p>I will step through the specific action for creating the call, as it isn&amp;rsquo;t straight forward. Further, Postman is still your friend. The only way I managed to get this configured is relying on this great tool.&lt;/p>
&lt;p>Using Postman, the format of the JSON can be defined, based on the API reference for &lt;a class="link" href="https://docs.microsoft.com/en-us/dynamics365/customer-engagement/web-api/phonecall?view=dynamics-ce-odata-9" target="_blank" rel="noopener"
>Phonecalls&lt;/a>. This highlights that activity parties are created, and associated with the call to create the phonecall.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;subject&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;CC Test&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scheduledend&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2019-05-01 12:00&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;regardingobjectid\_contact@odata.bind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/contacts(A651968A-5660-E911-A973-000D3A3ACAF8)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;directioncode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scheduleddurationminutes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;phonenumber&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;phonecall\_activity\_parties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">\&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;partyid\_contact@odata.bind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/contacts(A651968A-5660-E911-A973-000D3A3ACAF8)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;participationtypemask&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;partyid\_systemuser@odata.bind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/systemusers(C3AE1146-AD6D-E911-A984-000D3A3AC0C2)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;participationtypemask&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">\&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Take this JSON body and copy into a new action in your custom connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-2.png"
loading="lazy"
>&lt;/p>
&lt;p>Enter details in the general page, just enough to uniquely identify your action. Of course, you need to be a bit more descriptive if you plan on shipping out the connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-3.png"
loading="lazy"
>&lt;/p>
&lt;p>Select Import from Sample, Select Post, the URL should be just PhoneCalls, paste the JSON above into the body&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-4.png"
loading="lazy"
>&lt;/p>
&lt;p>Flow does some magic and now you can update your connector. I tried testing this, but got a little confused (me of little brain) so I assumed that Flow is good and would handle it and went straight to using the connector in Flow.&lt;/p>
&lt;p>Back in Flow, select a new action, custom connector, the new one you just created and the action established.&lt;/p>
&lt;p>The populate as below. I know the parameters are not the best names, someone with a bit more time would have tidied this up.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-5.png"
loading="lazy"
>&lt;/p>
&lt;p>These are the same formulas that were used earlier. You need to add the &amp;ldquo;/contacts(&amp;rdquo; etc to each GUID as the data bind requires it.&lt;/p>
&lt;p>Run the Flow and just like that, the Phone Call is created in D365 with a Call To correctly established.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/05-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>Call From is missing, IFTTT doesn&amp;rsquo;t let the Web call know who called it. I have searched, but IFTTT is meant for home grown activity, so if you have registered the app, you should know the call. The easiest way around this is to pass in a hardcoded value (email) specific to the end user so a System user can be looked up and populated in the parties field.&lt;/p></description></item><item><title>Alexa, Field Service and Me (Part 3) - Creating Work Orders</title><link>https://linked365.blog/2019/04/25/alexa-field-service-and-me-part-3-creating-work-orders/</link><pubDate>Thu, 25 Apr 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/04/25/alexa-field-service-and-me-part-3-creating-work-orders/</guid><description>&lt;img src="https://linked365.blog/images/2019/04-image-31.png" alt="Featured image of post Alexa, Field Service and Me (Part 3) - Creating Work Orders" />&lt;p>This is a continuation of my series on a proof of concept to allow Alexa to interact with D365 Field Service&lt;/p>
&lt;h2 id="objectives">Objectives&lt;/h2>
&lt;ul>
&lt;li>The Business scenario (&lt;a class="link" href="https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/" >Part 1&lt;/a>&lt;/li>
&lt;li>Create an Alexa Skill (&lt;a class="link" href="https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/" >Part 1&lt;/a>)&lt;/li>
&lt;li>Connect the Skill to D365 (&lt;a class="link" href="https://linked365.blog/2019/04/20/alexa-field-service-and-me-part-2-linking-to-flow/" >Part 2&lt;/a>)&lt;/li>
&lt;li>Use Field Service to book an appointment (This Part)&lt;/li>
&lt;li>Return the information to Alexa (&lt;a class="link" href="https://linked365.blog/2019/04/20/alexa-field-service-and-me-part-2-linking-to-flow/" >Part 2&lt;/a>)&lt;/li>
&lt;/ul>
&lt;p>In this final (unless I need to expand on the scenarios) part of the story, Flow will be used to take the information garnered from the user and create a work order.&lt;/p>
&lt;h2 id="calling-a-sub-flow">Calling a Sub-flow&lt;/h2>
&lt;p>The original flow was all about Alexa, but what about other voice assistants? Flow is no different that any other programming languages and as such we can use the same concepts to writing decent flows, one of them being re-usability. If the code to write the Work order and book it is generic, it can be re-used when Google or Siri is connected to the application.&lt;/p>
&lt;p>To this end, the Flow starts with another HTTP trigger, which is called from the previous flow.&lt;/p>
&lt;p>Just like when connection Flow to Alexa, the URL is required in the child to update the caller. Create a new Flow, using the When a HTTP request is received. As content to the JSON Schema, add just enough to get you going and use the output to send an email so that the schema coming from the parent flow can be seen. This is a repart of the logic used to start our parent flow.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-34.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-35.png"
loading="lazy"
>&lt;/p>
&lt;p>Once saved, the URL will be generated in the trigger step. Use this to call the child Flow from the parent. This is the HTTP action. The Method is a POST, the URI is the trigger URL defined in the child Flow. No headers are required. In the body, add in the things that are already known, just to ensure repeat calls to D365 are made. The Intent is also passed in, which has all the details about what the user wanted.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-55.png"
loading="lazy"
>&lt;/p>
&lt;p>Now ready for testing, ensure both Flows are in Test mode and trigger Alexa. After a little delay, an email should be sent in the child Flow detailing enough information to create a Work Item.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-56.png"
loading="lazy"
>&lt;/p>
&lt;p>Use this email to populate the JSON as previously, easily creating the schema that is required.&lt;/p>
&lt;h2 id="creating-a-work-order">Creating a Work Order&lt;/h2>
&lt;p>Next, get the Account the contact is associated with. This is done with a call to the D365 Instance using the Common Data Service Connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-57.png"
loading="lazy"
>&lt;/p>
&lt;p>A Work Order needs some fields to be set before it can be save, Work Order Type is one of them. This could be hard coded, but Alexa has supplied the intent. To match the Work order type with the intention in Alexa, a field on the Work Order Type was added, Alexa Intent, which is searched for in the CDS List Records action.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-58.png"
loading="lazy"
>&lt;/p>
&lt;p>To make the Flow easier to manage and reduce the dual looping, the Work Order Type is returned to a variable&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-60.png"
loading="lazy"
>&lt;/p>
&lt;p>Once the data for the new Work Order is available, create the Work Order using the CDS Create Record connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-61.png"
loading="lazy"
>&lt;/p>
&lt;p>Most of these are obvious, but the one that is not is the Work Order Number. In Field Service, this is automated, with a prefix and a number range. As this doesn&amp;rsquo;t work in Flow, a work number is generated using a random number, using an expression.&lt;/p>
&lt;p>rand(10000,100000)&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-62.png"
loading="lazy"
>&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-63.png"
loading="lazy"
>&lt;/p>
&lt;p>A few other parts of the work order are populated, helping the service manager to match the time as appropriate.&lt;/p></description></item><item><title>Alexa, Field Service and Me (Part 2) - Linking to Flow</title><link>https://linked365.blog/2019/04/20/alexa-field-service-and-me-part-2-linking-to-flow/</link><pubDate>Sat, 20 Apr 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/04/20/alexa-field-service-and-me-part-2-linking-to-flow/</guid><description>&lt;img src="https://linked365.blog/images/2019/04-image-31.png" alt="Featured image of post Alexa, Field Service and Me (Part 2) - Linking to Flow" />&lt;p>This is a continuation of my series on a proof of concept to allow Alexa to interact with D365 Field Service&lt;/p>
&lt;h2 id="objectives">Objectives&lt;/h2>
&lt;ul>
&lt;li>The Business scenario (&lt;a class="link" href="https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/" >Part 1&lt;/a>)&lt;/li>
&lt;li>Create an Alexa Skill (&lt;a class="link" href="https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/" >Part 1&lt;/a>)&lt;/li>
&lt;li>Connect the Skill to D365 (This part)&lt;/li>
&lt;li>Use Field Service to book an appointment (&lt;a class="link" href="https://linked365.blog/2019/04/25/alexa-field-service-and-me-part-3-creating-work-orders/" >Part 3&lt;/a>)&lt;/li>
&lt;li>Return the information to Alexa (This part)&lt;/li>
&lt;/ul>
&lt;p>In this post I will be linking Alexa to D365 and returning some information back to the end user.&lt;/p>
&lt;h2 id="receiving-information-from-alexa">Receiving information from Alexa&lt;/h2>
&lt;p>Alexa interacts with the outside world with a HTTPS request. Once Alexa has determined that it understands the user and they have asked for something of your skill, it posts to the web service you have configured.&lt;/p>
&lt;p>That API Guy had a great &lt;a class="link" href="https://thatapiguy.tech/2019/03/11/alexa-ask-microsoft-flow-to-read-out-my-latest-e-mail/" target="_blank" rel="noopener"
>post&lt;/a> where he links his Alexa to his O365 email account and has Alexa read out his new email. This article steps through linking Alexa and Flow, and it showed me how simple that part of the integration is. Microsoft has done the hard work by certifying it&amp;rsquo;s connections, you just need to create one.&lt;/p>
&lt;p>Flow provides several methods of subscribing to HTTP events, but the one we are interested in is at the bottom&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-33.png"
loading="lazy"
>&lt;/p>
&lt;p>The URL is generated for you, and is the bit you need to post into Alexa Skill configuration once we have created the Flow. In the JSON schema, we just want a stub for now. The schema is defined by Alexa and how you configure the skill. It is best to get the connection up and running and use the initial call to substitute the schema later.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-34.png"
loading="lazy"
>&lt;/p>
&lt;p>Every Flow needs at least one action to be able to save, so to aid testing and understand the JSON sent by Alexa, the first step is to send an email to myself with the content of the call.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-35.png"
loading="lazy"
>&lt;/p>
&lt;p>Saving the flow and go back to the trigger&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-36.png"
loading="lazy"
>&lt;/p>
&lt;p>A specific trigger URL for our Flow is now generated.&lt;/p>
&lt;h3 id="back-in-alexa">Back in Alexa&lt;/h3>
&lt;p>In Alexa, on the left in the Skill is Endpoints. Alexa allows different endpoints to the skill depending on the Alexa region as well as a fall back. As this is a POC, using the default is appropriate.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-37.png"
loading="lazy"
>&lt;/p>
&lt;p>The important part of this is the drop down below the URL copied from Flow, this needs to be the second option &amp;ldquo;&lt;em>My development endpoint is a sub-domain of a domain that has a wild card certificate from a certificate authority&lt;/em>&amp;rdquo;. Basically, Microsoft has certified all their Flow endpoints with wild card certificates, allowing Amazon to trust that it is genuine.&lt;/p>
&lt;p>One saved, Build your skill again. I found every time I touched any configuration or indeed anything in Alexa, I needed to rebuild.&lt;/p>
&lt;h2 id="testing">Testing&lt;/h2>
&lt;p>Ready to test. Alexa allows you to test your connection via the Test tab within your skill.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-38.png"
loading="lazy"
>&lt;/p>
&lt;p>You have to select Development from the drop down. You can also use this interface to check Production skills.&lt;/p>
&lt;p>A word of warning, everytime you build, to test that new build, I found I had to re-select Development in this window by toggling between &amp;ldquo;Off&amp;rdquo; and &amp;ldquo;Development&amp;rdquo;.&lt;/p>
&lt;p>Back in Flow, test your flow by confirming that you will conduct the action.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-39.png"
loading="lazy"
>&lt;/p>
&lt;p>In the Test panel in Alexa, enter a phrase with your Invocation and Utterance in Alexa entry and if Alexa understands it is for your invocation, your flow should be triggered! Alexa will complain, as our simple flow hasn&amp;rsquo;t returned anything to it. We&amp;rsquo;ll worry about that later.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-40.png"
loading="lazy"
>&lt;/p>
&lt;p>In our simple Flow, I used email as the action, as can be seen below.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-41.png"
loading="lazy"
>&lt;/p>
&lt;p>This is the raw JSON output of the Alexa skill and this is used to tell Flow what to expect. This way it will provide the rest of the Flow with properties that are more appropriate.&lt;/p>
&lt;p>Back in the Flow trigger, select Use sample payload to generate schema, which presents the text entry, where you paste in the email body.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-42.png"
loading="lazy"
>&lt;/p>
&lt;p>Flow does the hard work now and presents you with a schema for what Alexa is sending your Flow&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-43.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="authenticating-the-user">Authenticating the user&lt;/h2>
&lt;p>Taking triggers from Alexa, hence a user is all well and good, but they expect a rapid response. Alexa Skills are public domain once you publish it, anyone can enable your skill, hence a little work needs to be done to understand who is calling our skill and whether Big Energy Co supports them.&lt;/p>
&lt;h3 id="which-user-is-it">Which User is it?&lt;/h3>
&lt;p>The request from Alexa does include a unique reference of the user, but normally businesses like Big Energy Co work on email addresses, which you can get from Alexa, but the user has to give you permission. This can be pre-approved when the end user installs the skill or you can ask for approval.&lt;/p>
&lt;p>Alexa needs to be told that your skill would like this permission. This allows Alexa to request this permission for you when your skill is enabled by the user&lt;/p>
&lt;p>On the left hand menu, there is a Permissions link, where our skill asks for the email address&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-45.png"
loading="lazy"
>&lt;/p>
&lt;p>To ask Alexa for an users email address is a seperate web call with properties from original message sent from Alexa in the trigger.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-44.png"
loading="lazy"
>&lt;/p>
&lt;p>Alexa provides each session an access token so that Flow can ask for more information from Alexa in the context of the users session, location, names, email etc. As this is a globally distributed system, the api End point can vary depending on where the user started their session. The URI at the end asks for the email address of the user of the session.&lt;/p>
&lt;p>Alexa may refuse to return the email address, because the end user has not given permission for our skill to share this information. If the call to get the email was a success, it means that Alexa has returned the value and the Flow can continue&lt;/p>
&lt;p>Without the permission, the call fails and so the Flow captures this. Remember to set a run after for this step to ensure it still runs on failure. Otherwise the flow will fail not so gracefully.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-46.png"
loading="lazy"
>&lt;/p>
&lt;p>Alexa provides standard permission request functionality, the Flow responds to the original call with this detail in a JSON formatted string, with the permissions the Skill wants listed. Finally, for this failure, let the Flow terminate successfully. The Flow can&amp;rsquo;t continue without the information.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-47.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="does-big-energy-co-support-the-user">Does Big Energy Co support the user?&lt;/h3>
&lt;p>It is not enough that the user has asked us for help, they need to be known to Big Energy, as a contact. Querying D365 for this data is the next step&lt;/p>
&lt;p>Using the D365 connector, query the contact entity for the email address that Alexa has given us.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-48.png"
loading="lazy"
>&lt;/p>
&lt;p>A conditional flow checks to see if the return from the Retrieve Contact step has a single record using an expression below&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-54.png"
loading="lazy"
>&lt;/p>
&lt;p>length(body(&amp;lsquo;Retrieve_Contact&amp;rsquo;)?[&amp;lsquo;value&amp;rsquo;])&lt;/p>
&lt;h2 id="responding-to-the-user-quickly">Responding to the user, quickly&lt;/h2>
&lt;p>Like any Alexa Skill, the user expects an immediate response. Flow in itself is quick, but when it comes to updating or creating records, it can take seconds. The timeout for your response to Alexa is 10 seconds, which isn&amp;rsquo;t a lot when you want to look up several things to create the appropriate work order.&lt;/p>
&lt;p>To get around this, respond to the user once you know you have all the information you need to create the appropriate records in D365. Here, the Flow responds if the contact is in our database. In production, you could do some more checks or just assume that if the contact is known, the logic could create an opportunity to sell them a contract if nothing else. Equally, terminate gracefully after responding that Big Energy Co doesnt know the customer, prompting them to ring the service desk.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-51.png"
loading="lazy"
>&lt;/p>
&lt;p>In the switch statement, a response to the user is built up. Specific, personalised responses using the data you have retrieved is essential to give the customer an understanding that you have received the request and will respond.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-50.png"
loading="lazy"
>&lt;/p>
&lt;p>The responses are short and to the point but personalised to the customer and what they asked for with some expressions to add more information if they told Alexa.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-52.png"
loading="lazy"
>&lt;/p>
&lt;p>This snippet adds after &amp;ldquo;request for a Service&amp;rdquo; what the user asked for a service on, relying on the JSON formatted values.&lt;/p>
&lt;p>if(equals(triggerBody()?[&amp;lsquo;request&amp;rsquo;]?[&amp;lsquo;intent&amp;rsquo;]?[&amp;lsquo;slots&amp;rsquo;]?[&amp;lsquo;device&amp;rsquo;]?[&amp;lsquo;value&amp;rsquo;], &amp;lsquo;&amp;rsquo;), &amp;lsquo;&amp;rsquo;, concat(&amp;rsquo; for your &amp;lsquo;,triggerBody()?[&amp;lsquo;request&amp;rsquo;]?[&amp;lsquo;intent&amp;rsquo;]?[&amp;lsquo;slots&amp;rsquo;]?[&amp;lsquo;device&amp;rsquo;]?[&amp;lsquo;value&amp;rsquo;]))&lt;/p>
&lt;p>This snippet adds to the end a sentence including the date that the user asked for.&lt;/p>
&lt;p>if(equals(triggerBody()?[&amp;lsquo;request&amp;rsquo;]?[&amp;lsquo;intent&amp;rsquo;]?[&amp;lsquo;slots&amp;rsquo;]?[&amp;lsquo;date&amp;rsquo;]?[&amp;lsquo;value&amp;rsquo;],&amp;rsquo;&amp;rsquo;),&amp;rsquo;&amp;rsquo;,concat( &amp;lsquo;We will endeavour to send an engineer on &amp;lsquo;,triggerBody()?[&amp;lsquo;request&amp;rsquo;]?[&amp;lsquo;intent&amp;rsquo;]?[&amp;lsquo;slots&amp;rsquo;]?[&amp;lsquo;date&amp;rsquo;]?[&amp;lsquo;value&amp;rsquo;]))&lt;/p>
&lt;p>Finally, for the Alexa response part, the Flow returns a response to the user. This combines the body with a little standard text. This is what Alexa will say. You can also add a response to the screen for those devices able to do that.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-53.png"
loading="lazy"
>&lt;/p>
&lt;p>The final part of this flow goes on and creates the work order. I seperated out the flows using a sub flow, which is discussed in the next part of the blog.&lt;/p></description></item><item><title>Alexa, Field Service and Me (Part 1)</title><link>https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/</link><pubDate>Fri, 19 Apr 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/04/19/alexa-field-service-and-me-part-1/</guid><description>&lt;img src="https://linked365.blog/images/2019/04-image-31.png" alt="Featured image of post Alexa, Field Service and Me (Part 1)" />&lt;p>As we all now have smart devices in our home, linking them to business applications could be a key differentiator between winners and the also rans. This series of posts will demonstrate how I connected Alexa to D365 Field service.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-31.png"
loading="lazy"
>&lt;/p>
&lt;h4 id="objectives">Objectives&lt;/h4>
&lt;ul>
&lt;li>The Business scenario (this part)&lt;/li>
&lt;li>Create an Alexa Skill (this part)&lt;/li>
&lt;li>Connect the Skill to D365 (&lt;a class="link" href="https://linked365.blog/2019/04/20/alexa-field-service-and-me-part-2-linking-to-flow/" target="_blank" rel="noopener"
>Part 2&lt;/a>)&lt;/li>
&lt;li>Use Field Service to book an appointment (&lt;a class="link" href="https://linked365.blog/2019/04/25/alexa-field-service-and-me-part-3-creating-work-orders/" target="_blank" rel="noopener"
>Part 3&lt;/a>)&lt;/li>
&lt;li>Return the information to Alexa (&lt;a class="link" href="https://linked365.blog/2019/04/20/alexa-field-service-and-me-part-2-linking-to-flow/" target="_blank" rel="noopener"
>Part 2&lt;/a>)&lt;/li>
&lt;/ul>
&lt;h2 id="our-scenario---big-energy-co">Our Scenario - Big Energy Co&lt;/h2>
&lt;p>Our generic big energy company is diversifying into support and maintenance of home energy products, boilers, central heating, plumbing, electricals and numerous other aspects of a consumer&amp;rsquo;s home life. A client will ring up, tell the support desk that they have an issue with the appliance and the employee will book a suitable engineer in to come out to their home. This is all done via Field Service in D365. Big Energy also have scheduled servicing of Boilers on an annual basis.&lt;/p>
&lt;p>What the CTO wants to do is embrace the in home virtual assistant to promote Big Energy as a forward thinking organisation which is at the forefront of technology to allow Big Energy&amp;rsquo;s customers to book an engineers visit via their smart device. Her expectation is that Alexa, Google Home or Siri will allow a service call to be booked without talking to the support desk. This will allow meaningful interactions with their customers 24/7.&lt;/p>
&lt;p>The proof of concept will start with the front runner in the war of virtual assistance, mostly because I have one.&lt;/p>
&lt;h2 id="alexa">Alexa&lt;/h2>
&lt;p>If you have read my introduction to &lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365/" target="_blank" rel="noopener"
>LUIS&lt;/a> in Alexa Skill concepts are a mirror of the concepts introduced in LUIS. Alexa has Utterances &amp;amp; Intents. Entities are called Intent Slots for Alexa. Alexa has also got another concept, that being the Invocation.&lt;/p>
&lt;p>First off, get yourself an Amazon account &amp;amp; sign up for the developer program at &lt;a class="link" href="https://developer.amazon.com/" target="_blank" rel="noopener"
>developer.amazon.com.&lt;/a> This is all free. People can buy skills, but not sure Big Energy&amp;rsquo;s customers would think this is appropriate.&lt;/p>
&lt;p>Use the Create Skill button, enter a name &amp;amp; select a language. I also choose Custom &amp;amp; Provision your own to allow us to send data to Flow which is hosting our integration.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-23.png"
loading="lazy"
>&lt;/p>
&lt;p>Then I select Start from scratch, none of the others seem to match our scenario&lt;/p>
&lt;h3 id="invocation">Invocation&lt;/h3>
&lt;p>An Invocation is the starting point and is the differentiator between you and every other Alexa skill out there. It is effectively a name for your skill, what the user would say to call your skill. I have slightly altered my invocation to put some spaces in there so it is more natural to the end user.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-24.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="intents">Intents&lt;/h3>
&lt;p>Like in LUIS, intents are a category or what the user is looking for or asking. You can have many intents per skill. In our scenario around home appliances the user can ask for a service, a repair or an emergency as a starting point. Let&amp;rsquo;s start with a request for service.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-25.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="utterances">Utterances&lt;/h3>
&lt;p>Utterances are samples to train Alex to understand the intent in natural language. Add as many utterances as you like as samples of how a person would ask for a service or repair, but ensure they are different.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-26.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="slots">Slots&lt;/h3>
&lt;p>In my sample utterances above you can see I have added Slots. Slots are the same as entities in LUIS, data that the user gives us when they are saying their utterance in addition to the type of thing they want.&lt;/p>
&lt;p>Each Slot has a type, and I have used the default Amazon types except for device, which is a custom one.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-28.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="slot-types">Slot Types&lt;/h3>
&lt;p>Amazon has 43 of it&amp;rsquo;s own list types or 6 built in types for numbers &amp;amp; dates, but devices are not in the list. I want to know what type of thing the engineer is going out to fix, not sure I need to be that specific, but would be good to know if I need to send an electrician, gas engineer or plumber. I have added my own Slot Type, called it device and I now list the values I expect.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-29.png"
loading="lazy"
>&lt;/p>
&lt;p>You also should also enter synonyms, not everyone calls it a telly, just us northeners.&lt;/p>
&lt;p>Once you have entered enough intents &amp;amp; utterances, time to build and test. A nice message lets you know when it is done building. Ready for the utterance profiler or testing&lt;/p>
&lt;h2 id="utterance-profiler">Utterance Profiler&lt;/h2>
&lt;p>In the top right, is a pop out to test your utterances. Enter an utterance you haven&amp;rsquo;t used before to check you are getting the expected results.&lt;/p>
&lt;p>The text I wrote is in blue, it has decided that this is a service intent, with a device of boiler &amp;amp; the day of wednesday.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-30.png"
loading="lazy"
>&lt;/p>
&lt;p>You can carry on fine tuning your utterances, intents and slots to get the most accurate model. Like any language understanding, this will be an ongoing model.&lt;/p>
&lt;p>This is Alexa done, we have configured everything we need to in Alexa, apart from our link to Flow. This needs a bit of pre-work in Flow to activate, in the next &lt;a class="link" href="https://linked365.blog/2019/04/20/alexa-field-service-and-me-part-2-linking-to-flow/" target="_blank" rel="noopener"
>post&lt;/a>.&lt;/p></description></item><item><title>Connecting LUIS &amp; D365 (part 4) - Custom Connector</title><link>https://linked365.blog/2019/04/03/connecting-luis-d365-part-4-custom-connector/</link><pubDate>Wed, 03 Apr 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/04/03/connecting-luis-d365-part-4-custom-connector/</guid><description>&lt;img src="https://linked365.blog/images/2019/04-luisd365-e1554192911124.jpg" alt="Featured image of post Connecting LUIS &amp; D365 (part 4) - Custom Connector" />&lt;p>In my previous &lt;a class="link" href="https://linked365.blog/2019/04/02/connecting-luis-to-d365-part-3/" >post&lt;/a>, the fact that I had to &amp;ldquo;fudge&amp;rdquo; the routing and closure of the case irked me. Whilst at D365 EU Summit in Amsterdam, I spent an hour listening to Serge Luca (his excellent blog is &lt;a class="link" href="https://sergeluca.wordpress.com/" target="_blank" rel="noopener"
>here)&lt;/a>. He inspired me to think that we could close this loop using a Custom Connector in Flow connected to our D365 instance, allowing an API call directly to the closure action.&lt;/p>
&lt;p>Back in the UK, I did some digging, found another excellent blog from Andrew Butenko which steps through doing this for Won Opportunity &lt;a class="link" href="https://butenko.pro/2018/11/30/calling-d365-actions-functions-from-flow/" target="_blank" rel="noopener"
>here&lt;/a>, so just to complete the circle I will step through how I did this.&lt;/p>
&lt;h2 id="create-the-app-in-azure">Create the App in Azure&lt;/h2>
&lt;p>This is a bit heavy for a citizen developer &amp;amp; in the real world you will have to ask your O365 admin to do this, but in my trail I have the appropriate access.&lt;/p>
&lt;p>Go to &lt;a class="link" href="http://portal.azure.com" target="_blank" rel="noopener"
>portal.azure.com&lt;/a> &amp;amp; log in using your CRM Admin credentials.&lt;/p>
&lt;p>In the left pane, select Azure Active Directory, then App Registrations (Preview) then New registration. Give it a name, select Accounts in this organizational directory only &amp;amp; enter localhost as a temporary redirect URL, this will change later. Select Register &amp;amp; allow Azure to do it&amp;rsquo;s stuff&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-3.png"
loading="lazy"
>&lt;/p>
&lt;p>On the next screen, select API Permissions, then Add a permission&lt;/p>
&lt;p>The one we are interested in is Dynamics CRM. Select it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-4.png"
loading="lazy"
>&lt;/p>
&lt;p>Check the box next to user_impersonation. Select Add permissions &amp;amp; now you should have 2 permissions for your new application. Now we need to ensure only we can access it.&lt;/p>
&lt;p>In Certificates &amp;amp; secrets, select New client secret. Give it a Description and decide how long you want to wait before it expires. Not an issue for our demo, our trial wont last as long as this expiry. Once created, make sure you copy the value information, you won&amp;rsquo;t be able to retrieve this at a later stage.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-5.png"
loading="lazy"
>&lt;/p>
&lt;p>We also need the application id, this is available on the Overview tab&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-6.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="create-a-custom-connector">Create a Custom Connector&lt;/h2>
&lt;p>Back in flow, under Data &amp;amp; then Custom Connectors there is a Create custom connector drop down, select Create from blank and give it a name.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>On the next screen, you can customise the description &amp;amp; logo if you want, but the important bit is the Host, which is your instance url, and the Base URL which is the api access path.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-11.png"
loading="lazy"
>&lt;/p>
&lt;p>Select Security next &amp;amp; choose OAuth 2.0. Identity Provider should be Azure Active Directory, Client Id is the Application Id from previous &amp;amp; Client Secret is also from the previous step. Login URL, Tenant ID &amp;amp; Scope should be left to their default. Resource URL is the url to your environment.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-9.png"
loading="lazy"
>&lt;/p>
&lt;p>Save the connector by using the Update Connector now. There is an important piece of information, the Redirect URL, which we have to copy &amp;amp; add to our connector back in Azure.&lt;/p>
&lt;p>Select the App Registration in Azure, select Redirect URIs&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-12.png"
loading="lazy"
>&lt;/p>
&lt;p>Add a new one from your copied redirect (I think it is standard, as I have only seen one, but just to be sure)&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-13.png"
loading="lazy"
>&lt;/p>
&lt;p>The next screen allows you to add Actions &amp;amp; triggers. I think there could be a whole new blog post on using triggers this way. As custom connector is generic across all apis, the flexibility is great. Select New Action.&lt;/p>
&lt;p>The General section describes the connector in Flow, so useful but nothing special is required apart from the Operation Id, which is what api action you want to call &amp;amp; must be unique within each connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-15.png"
loading="lazy"
>&lt;/p>
&lt;p>Select Import from sample. This would usually be a sample JSON function. I built mine using Postman, and this is a bit more than a citizen developer would be able to handle without experience. Federico Jousset has a great &lt;a class="link" href="http://www.fedejousset.com/2018/10/03/dynamics-365-web-api-postman-collection/" target="_blank" rel="noopener"
>blog pos&lt;/a>t on using Postman to &amp;ldquo;play&amp;rdquo; with D365 end points, including a link to an extensive Postman collection with lots of examples which were the key to understanding the call, particularly the parameters.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-16.png"
loading="lazy"
>&lt;/p>
&lt;p>When you hit import, you will get this section under Request. The bit we are interested in is at the bottom, the body.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-17.png"
loading="lazy"
>&lt;/p>
&lt;p>Click on body, then edit you will see a list of the parameters that Flow inferred from the sample body we gave it.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-18.png"
loading="lazy"
>&lt;/p>
&lt;p>Select Incident Id for example, we can now give it a little more information &amp;amp; more importantly give the end user of the low more information with the Title &amp;amp; default value &amp;amp; whether it is required&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-19.png"
loading="lazy"
>&lt;/p>
&lt;p>We are now ready to Test. Select the test button, choose New Connection &amp;amp; connect to your D365 system. Enter the appropriate values, but be careful over the incident Id, as it is expecting a odata conversion. Someone better at connectors could tell me why.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-21.png"
loading="lazy"
>&lt;/p>
&lt;p>If everything is good, you will get a blank body response back, as this is how the CloseIncident works&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-20.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="using-the-connector-in-flow">Using the connector in Flow&lt;/h2>
&lt;p>In the previous article, I showed you how we had to use a custom field &amp;amp; workflow in D365 to close the incident. I can now replace that with my custom connector call&lt;/p>
&lt;p>Where I was updating a record because they were asking for their next bill, I remove the change to the CloseByFlow field and replace it with a seperate call&lt;/p>
&lt;p>In Flow, I add a new action, select the Custom Tab, my connector &amp;amp; the action I want, this case the CloseIncident. We need to mimic how the api is expceting the Case Id to be passed, which is why we send with the &amp;lsquo;/incidents(&amp;rsquo; prefix &amp;amp; &amp;lsquo;)&amp;rsquo; suffix.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-22.png"
loading="lazy"
>&lt;/p>
&lt;p>Test the flow &amp;amp; we have now completed the scenario. Case closure is now part of the flow.&lt;/p>
&lt;p>Custom connectors are new for Flow &amp;amp; give the Flow creator that step extra of functionality where the OOTB solutions don&amp;rsquo;t work.&lt;/p>
&lt;p>There is a word of caution about licensing here, I have been using a trial of E5, which gives you Flow for Office 365 License. I can create a custom connector to a standard application but not a custom application with it. This is the same for Flow for Dynamics 365, so I don&amp;rsquo;t need an extra license, but be warned.&lt;/p></description></item><item><title>Connecting LUIS to D365 (part 3)</title><link>https://linked365.blog/2019/04/02/connecting-luis-to-d365-part-3/</link><pubDate>Tue, 02 Apr 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/04/02/connecting-luis-to-d365-part-3/</guid><description>&lt;img src="https://linked365.blog/images/2019/04-luisd365-e1554192911124.jpg" alt="Featured image of post Connecting LUIS to D365 (part 3)" />&lt;p>In this 3rd installment of the series devoted to using Language Understanding (LUIS) to categorise emails via Flow, I will describe some of the parts of the D365 solution and walk through a final version of the Flow to extend its capabilities.&lt;/p>
&lt;p>This blog has at least 3 parts, but overall the posts objectives are&lt;/p>
&lt;ul>
&lt;li>Give you an understanding of a real-life scenario where we could implement LUIS – (&lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365-part-1" >Part 1&lt;/a>)&lt;/li>
&lt;li>Introduce you to the concepts of LUIS (&lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365-part-1" >Part 1&lt;/a>)&lt;/li>
&lt;li>Discuss the Microsoft Flow to connect LUIS to D365 (&lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-to-d365-part-2/" >Part 2&lt;/a>)&lt;/li>
&lt;li>Give you some next steps on how you can try to bring LUIS into your organisation&lt;/li>
&lt;/ul>
&lt;h2 id="routing-the-case">Routing the case&lt;/h2>
&lt;p>Standard Save and Route functionality only fires automatically on case creation, or when a user manually hits the button. This action is available in workflow, but not in the CRUD actions of the D365 or CDS connectors. To get around this there is a workflow triggered of the change of the subject field which calls the OOTB action ApplyRoutingRule. This effectively is the same as the user clicking the button.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-16.png"
loading="lazy"
>&lt;/p>
&lt;p>The routing rules are very simple, routing the case based on the subject selected.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-17.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="adding-information-from-the-case">Adding information from the case&lt;/h2>
&lt;p>In our scenario, based around a support desk for a large power company, one of the common requests is to provide meter readings. This data is invaluable.&lt;/p>
&lt;p>In LUIS, this information is called an Entity. You can configure entities for each of the Intents to denote some parcel of information that the user is telling you on top of the intent of their email.&lt;/p>
&lt;p>As you start typing and enter data, LUIS recognises this and replaces the data with a data type that it will use.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-18.png"
loading="lazy"
>&lt;/p>
&lt;p>In Flow, the return from LUIS will contain a list of all the entities it has found in your utterance. The flow then updates the case with this defined data, in a separate field on the case record.&lt;/p>
&lt;p>The first part of the flow checks the length of the array of entities that LUIS returned. If there are no entities, then just do a normal update of the case record. If there are entities, loop through them&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-19.png"
loading="lazy"
>&lt;/p>
&lt;p>As the entities array is looped through, check that the entity is a number. LUIS could return you a date or name. If it is not a number, just do a normal update of the record.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-20.png"
loading="lazy"
>&lt;/p>
&lt;p>If it is a number, pass the meter reading to the case update in the new field on the Case entity.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-21.png"
loading="lazy"
>&lt;/p>
&lt;p>This approach does lead to duplicate updates, 1 for each entity it finds. You may be better suited to extend the logic in a production system to prevent this.&lt;/p>
&lt;h2 id="auto-reply-for-common-intents">Auto-reply for common intents&lt;/h2>
&lt;p>In our scenario, one of the common requests that the help desk gets is to ask when the end user is due a bill. This again could be dealt with in numerous ways via workflow in D365, but doing it in Flow allows the demonstration of Flow&amp;rsquo;s abilities.&lt;/p>
&lt;p>The first part is to check the Top scoring intent is &amp;ldquo;Next Bill due&amp;rdquo;. The flow goes on to check for meter readings if it isn&amp;rsquo;t.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-22.png"
loading="lazy"
>&lt;/p>
&lt;p>Using D365 connector this time (Microsoft changes so quickly, it is hard to keep up with what the best methods are) I retrieve the contact that created the case then send them a nicely formatted email. This will send the email as the account you attach to run the flow, in our scenario the support desk. I have only hardcoded the content here, but you could easily read the date of the next bill from the users account record or something similar.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-23.png"
loading="lazy"
>&lt;/p>
&lt;p>The next step is close the case as we have provided the end user with the appropriate information. In D365, this is another action not readily available via the CRUD of the CDS or D365 connector.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-24.png"
loading="lazy"
>&lt;/p>
&lt;p>I now realise, after attending the EU D365 User Group Summit that this would be possible via Flow, along with routing the case, if you create your own custom connector and associate that with your D365 API instance, but that&amp;rsquo;s for another post.&lt;/p>
&lt;p>To get round the limitations, a new field is populated on the Case record, Close from Flow.&lt;/p>
&lt;p>A workflow is then triggered when this field changes to call the OOTB action if the value is true.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-25.png"
loading="lazy"
>&lt;/p>
&lt;h2 id="sentiment-analysis">Sentiment analysis&lt;/h2>
&lt;p>One of the big uses for social engagement and monitoring is to assess sentiment in the comments made by customers. LUIS provides this information in a textual (positive, neutral or negative) as well as numeric form if enable it on the application.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/04-image-26.png"
loading="lazy"
>&lt;/p>
&lt;p>Once you have this configured, recording this against the case and maybe setting a priority on it&amp;rsquo;s value would be quite straight forward.&lt;/p></description></item><item><title>Connecting LUIS &amp; D365 (part 1)</title><link>https://linked365.blog/2019/03/31/connecting-luis-d365-part-1/</link><pubDate>Sun, 31 Mar 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/03/31/connecting-luis-d365-part-1/</guid><description>&lt;img src="https://linked365.blog/images/2019/03-luisd365-e1554192911124.jpg" alt="Featured image of post Connecting LUIS &amp; D365 (part 1)" />&lt;p>As Microsoft wants us to consider using AI in all aspects of our developments, I wanted to see if I could use one of the cognitive services available to life in a real life scenaro.&lt;/p>
&lt;p>This post &amp;amp; it&amp;rsquo;s other parts will&lt;/p>
&lt;ul>
&lt;li>Give you an understanding of a real-life scenario where we could implement LUIS&lt;/li>
&lt;li>Introduce you to the concepts of LUIS&lt;/li>
&lt;li>Discuss the Microsoft Flow to connect LUIS to D365&lt;/li>
&lt;li>Give you some next steps on how you can try to bring LUIS into your organisation&lt;/li>
&lt;/ul>
&lt;h2 id="the-scenario---big-energy-co">The Scenario - Big Energy Co&lt;/h2>
&lt;p>Imagine a busy helpdesk or contact centre at one of the big electricity or gas companies. They are inundated with calls, emails &amp;amp; chats from their customer base to request numerous changes or additions to their account, provide meter readings, complain or numerous other concerns.&lt;/p>
&lt;p>Email is one of the biggest mediums for support when the matter is not urgent. The standard email to case conversion is commonaly used to bring these support requests into D365 CE.&lt;/p>
&lt;p>But that is traditionally where it stops. A 1st line support user would have to read the content of the case, decide on the appropriate subject for the new case &amp;amp; select save and route. This takes time, upto a couple of minutes, of invaluable time of an educated, talented member of your team.&lt;/p>
&lt;h2 id="luis---language-understanding">LUIS - Language Understanding&lt;/h2>
&lt;p>&lt;a class="link" href="http://www.luis.ai" target="_blank" rel="noopener"
>LUIS&lt;/a> is Microsoft&amp;rsquo;s cloud-based API which tries to understand the meaning behind phrases. LUIS could take a paragraph of text and categorise it to suggest what the writer of the paragraph is trying to say. Bringing machine learning into your app can drastically reduce the time spent scouring tet for meaning .&lt;/p>
&lt;p>In a 1st line support scenario, an email comes into a support mailbox and D365 will create a case with it&amp;rsquo;s auto-creation rules to allow a 1st line support person to read the email, decide what the email is concerning, then route the case to the relevant team.&lt;/p>
&lt;p>Using Flow and LUIS, we can automate this step, reducing the time taken to get the case to the right people, improving SLAs and reducing costs.&lt;/p>
&lt;h2 id="luis-configuration">LUIS Configuration&lt;/h2>
&lt;p>For all of this, I am using a trial environment, Microsoft makes it easy for you to play with all it&amp;rsquo;s tools, and LUIS is no exception. The free licenses gives you 5 text requests per seconds, definetly adequate for development and trial purposes. It is then £1.11 per 1000 transactions above this, with a max of 50 transactions a second.&lt;/p>
&lt;p>By going to &lt;a class="link" href="http://luis.ai" target="_blank" rel="noopener"
>luis.ai&lt;/a> &amp;amp; logging in, you will be allowed to create an app. This is the starting point. As you can see my app has been created already.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="intents">Intents&lt;/h3>
&lt;p>Intents are the term for meaning within the app. Each app will have several intents, what are the likely short description or categories of why an email would come into the environment?&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-2.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="utterances">Utterances&lt;/h3>
&lt;p>Each intent has several utterances, which are phrases from which LUIS will learn and define the intent of the text passed to it. It is very simple to add utterances and you should aim to get 15 or so for each intent as a starting point.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-3.png"
loading="lazy"
>&lt;/p>
&lt;p>You can add as many utterances as you like, try to keep your utterance count consist between intents as a higher utterance list on one of the intents will lead to it be chosen more, in effective giving you false results. When you get into a near production scenario, you need to keep adding them from feedback from your users. You could enact a process which, when LUIS fails to assocaite an intent to a case or gives a false classification, this is highlighted for a BA to analyse &amp;amp; apply to the model.&lt;/p>
&lt;p>The next thing to do is train LUIS. This brings any changes you have made into the AI so they also can be used to understand the conversation.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-4.png"
loading="lazy"
>&lt;/p>
&lt;p>Training is complete when you get a green bar across the top.&lt;/p>
&lt;p>Now we have a model, let&amp;rsquo;s test it, using the inbuilt Test functionality available from the top right.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-5.png"
loading="lazy"
>&lt;/p>
&lt;p>If you type a string in the box provided &amp;amp; hit return, you are calling LUIS and the text will be analysed and LUIS will reply with the highest intent &amp;amp; how likely the phrase enters matches it.&lt;/p>
&lt;p>I have entered the phrase &amp;ldquo;can you help me transfer my account&amp;rdquo;, which wasn&amp;rsquo;t part of the utterances I created earlier, but closely matches one of the Account closure utterances.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-6.png"
loading="lazy"
>&lt;/p>
&lt;p>Looking at the response, LUIS is stating that he thinks this phrase is about Account Closure and he is 58% sure that this is correct. If you click on the Inspect link you can use this to train LUIS if the response is not correct by using the Edit link, adding an utterance to one of the other intents or reenforcing the entered text was in the correct intent.&lt;/p>
&lt;h2 id="publishing-luis">Publishing LUIS&lt;/h2>
&lt;p>My LUIS app is ready, it is trained and raring to go. Now we need to allow access from the publicly available API. The Publish button steps you through the process, ensure you go to Production. Don&amp;rsquo;t worry about costs for now, you get 5 requests a second for free, which is great for trialling &amp;amp; developing&lt;/p>
&lt;p>In Application Information, there 2 key bits for connecting LUIS to Flow. Firstly, the Application ID is the key we want later. Secondly, we need to ensure anyone with the Application ID can access our endpoint &amp;amp; use the app we have configured, so ensure the Make this app public is checked.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>This is a little less secure than you would want in production and there are details about LUIS security available to ensure only the proper users can access fully documented &lt;a class="link" href="https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-security" target="_blank" rel="noopener"
>here&lt;/a>.&lt;/p>
&lt;p>That&amp;rsquo;s it, ready to go. In the next part, I will configure D365 and Flow to complete the scenario&lt;/p></description></item><item><title>Connecting LUIS to D365 (Part 2)</title><link>https://linked365.blog/2019/03/31/connecting-luis-to-d365-part-2/</link><pubDate>Sun, 31 Mar 2019 00:00:00 +0000</pubDate><guid>https://linked365.blog/2019/03/31/connecting-luis-to-d365-part-2/</guid><description>&lt;img src="https://linked365.blog/images/2019/04-luisd365-e1554192911124.jpg" alt="Featured image of post Connecting LUIS to D365 (Part 2)" />&lt;p>In the first &lt;a class="link" href="https://linked365.blog/2019/03/25/connecting-luis-d365/" target="_blank" rel="noopener"
>part&lt;/a> of this blog post I explained our scenario for Big Energy Co and it&amp;rsquo;s desire to improve efficiency in the support desk, introduced LUIS and the core concepts around utterances and intents, configured LUIS to allow it to understand the intents we need and published the application.&lt;/p>
&lt;p>Now I need to connect LUIS and D365 to allow automatic classification and routing of cases based on Natural Language inspection of the body of the case.&lt;/p>
&lt;p>This blog has at least 3 parts, but overall the posts objectives are&lt;/p>
&lt;ul>
&lt;li>Give you an understanding of a real-life scenario where we could implement LUIS (&lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365/" target="_blank" rel="noopener"
>Part 1&lt;/a>)&lt;/li>
&lt;li>Introduce you to the concepts of LUIS (&lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365/" target="_blank" rel="noopener"
>Part 1&lt;/a>)&lt;/li>
&lt;li>Discuss the Microsoft Flow to connect LUIS to D365&lt;/li>
&lt;li>Give you some next steps on how you can try to bring LUIS into your organisation&lt;/li>
&lt;/ul>
&lt;p>Nowadays, Microsoft &lt;a class="link" href="https://flow.microsoft.com/" target="_blank" rel="noopener"
>Flow&lt;/a> doesn&amp;rsquo;t need an introduction - it is the cornerstone for the whole of Microsoft Power suite (not sure why it doesn&amp;rsquo;t get the Power prefix) and allow &lt;em>spot&lt;/em> integrations between one or more disparate applications. It also allows you to &amp;ldquo;program&amp;rdquo; some data transformation or logic to control how data flows between applications or actions to take in specific circumstances. Flow is a complex beast, but simple to use in a lot of cases.&lt;/p>
&lt;h2 id="the-flow">The Flow&lt;/h2>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image.png"
loading="lazy"
>&lt;/p>
&lt;p>As you can see, the flow is pretty simple. We get a trigger from D365, strip the body of the case down to raw text, check to see if have anything left, ask LUIS to predict what it is, check how confident LUIS is, then update the case again. This is a simple flow which isn&amp;rsquo;t production ready by any means, but shows you the power of Flow and is enough for you to get started&lt;/p>
&lt;h3 id="triggering-from-d365">Triggering from D365&lt;/h3>
&lt;p>Our first step the trigger from D365&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-1.png"
loading="lazy"
>&lt;/p>
&lt;p>I am using the Common Data Service connectors here, which are the future, so why not? We have to connect to our D365 environment, chose the Cases entity and give it a scope. This is new for the CDS connector and allows a lot more flexibility around what data runs through a flow. You could keep the data to one business unit in an organisation, useful if you have country specific flows for example.&lt;/p>
&lt;h3 id="stripping-that-nasty-html">Stripping that nasty HTML&lt;/h3>
&lt;p>Email contains lots of formatting information that LUIS doesn&amp;rsquo;t understand. CSS, html tags, that heading etc will really upset LUIS, but handly there is an action to return just the actual words. Pass the body of the email, the case description to it.&lt;/p>
&lt;p>Also in this snippet, I am setting up a variable for use later on, to store the Subject ID.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-2.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="checking-for-empty-email">Checking for empty email&lt;/h3>
&lt;p>Another thing that LUIS doesnt handle is sending it an empty request. Just for a bit of house keeping, if, after stripping out all the html tags, the content is empty, rather than calling LUIS, exit gracefully. Always have in mind when running any flow that someone will be bombarded with error alerts if you don&amp;rsquo;t check&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-3.png"
loading="lazy"
>&lt;/p>
&lt;h3 id="passing-to-luis">Passing to LUIS&lt;/h3>
&lt;p>In the previous &lt;a class="link" href="https://linked365.blog/2019/03/31/connecting-luis-d365-part-1/#publishing-luis" >post&lt;/a>, when your LUIS app is published, you get a GUID representing the Application key. When you first connect to LUIS in Flow, this is what you need. If you have many applications in LUIS on that key, Flow wants to know which one, and also which version. This is handy if you are training LUIS and the new version isn&amp;rsquo;t quite ready, stick with a previous version of your app.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-4.png"
loading="lazy"
>&lt;/p>
&lt;p>The Utterance text is the email body, stripped of the html, which LUIS is to analyse.&lt;/p>
&lt;h3 id="is-luis-confident">Is LUIS confident?&lt;/h3>
&lt;p>Like any AI product, LUIS can only predict what it thinks is the likely intent of anything you pass to it. It provides you with a score, 0 being not confident, 1 being overly confident that matches each intent. We basically use this as a percentage. When this returns to flow, LUIS provides the top scoring intent as well as the top scoring intent score. If LUIS isn&amp;rsquo;t confident that what you have sent it, then Flow shouldn&amp;rsquo;t update the case. This case should stay with the 1st line queue for them to resolve.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-5.png"
loading="lazy"
>&lt;/p>
&lt;p>Again, this is about making your code production ready. It would be quite easy to trigger additional steps here to ensure this case is looked at and maybe a training point for LUIS after a human decides on the best route for the case.&lt;/p>
&lt;h3 id="retrieve-the-subject">Retrieve the subject&lt;/h3>
&lt;p>The subject tree in D365 matches the intents. This is a key point for our process to work. You could use other factors or matching, but this is pretty simple.&lt;/p>
&lt;p>Using the CDS link, we use the List Records feature to query the Subject entity. The filter query is the key here, using a piece of OData query notation to find the subject with a title that matches our top scoring intent.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-7.png"
loading="lazy"
>&lt;/p>
&lt;p>Though we will only retrieve one record, the List Records action will always return an array or records, an array of one. To keep your flow in one column, you can set a variable with the value of the subject Id you have retrieved and use this going forward. If not, you will have to do the remainder of these steps within the Apply to each action.&lt;/p>
&lt;h3 id="updating-the-case">Updating the Case&lt;/h3>
&lt;p>The last part of this simple flow is to update the case back in D365. Using the Id from the originally triggered case, we update parts of the case.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-8.png"
loading="lazy"
>&lt;/p>
&lt;p>The description is updated with the html stripped text. There are lots of ways to do this in D365, but seeing as we have done it already, it is neat to use it again. I have also added a new field on the case to log how confident LUIS is. This could be used as a summary later or for managing the training of LUIS.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-9.png"
loading="lazy"
>&lt;/p>
&lt;p>Finally, the subject Id is passed to the Subject field.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-10.png"
loading="lazy"
>&lt;/p>
&lt;p>So that is our flow complete. Now for some testing.&lt;/p>
&lt;h2 id="testing-our-flow">Testing our Flow&lt;/h2>
&lt;p>Flow has some really neat tools to ease testing. In the right corner, is the Test button which gives us this panel. The first time you run a flow, you will have to perform the trigger action, sending in an email to convert to a case or creating a case in our scenario, but after that, we can use previous data. Great for tweaking &amp;amp; checking&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-11.png"
loading="lazy"
>&lt;/p>
&lt;p>If you run a test, you will hopefully get a screen with lots of green ticks next to the parts that ran successfully as well as some crosses against those that didn&amp;rsquo;t.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-12.png"
loading="lazy"
>&lt;/p>
&lt;p>If you drill down to each step, you can see the input and the output from that action.&lt;/p>
&lt;p>In this example, you can see the original html formatted input from an email as well as the output of just the text.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-13.png"
loading="lazy"
>&lt;/p>
&lt;p>The call to LUIS shows the JSON return from LUIS. The LUIS connector gives a lot of properties transformed out of this content.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-14.png"
loading="lazy"
>&lt;/p>
&lt;p>The CDS record update shows only the fields that we update, a standard D365 API call. You can see the Subject ID being set, as well as the html stripped description and the Intent score.&lt;/p>
&lt;p>&lt;img src="https://linked365.blog/images/2019/03-image-15.png"
loading="lazy"
>&lt;/p>
&lt;p>So, flow done. In the next installment, I will show the D365 implementation, how we bring it all together. I also take LUIS and flow a little bit further, to add more complexity and functionality to the both to improve the scenario.&lt;/p></description></item></channel></rss>