Build Your Own ChatGPT-Style App Using Power Automate and Power Apps

Just for fun, I wanted to see how I could replicate a basic ChatGPT like experience using the tools available to me in the Power Platform. I will cover setting up the Power Automate flow to interface the OpenAI completions endpoint using GPT-4 Turbo and a Power App for the UI. In a future post, I will create another flow(s) to work with the Assistants API and take this to the next level.

A quick look at the app and how it works. The app will keep the chat context active using the completions endpoint until you want to clear it out. Like I said, it’s basic!

Here is what I used to get this up and running:
Power App
Power Automate flow with the HTTP connector
OpenAI account
OpenAI API key (credit card required)

Here is the layout of the flow and Power App


The key for this to work and keep the context of the conversation flowing is to pass all of the previous questions and responses back to the API each time a new question is asked. Yes, that can start to add up in terms of burning a lot of tokens for a large chat.

The input is converted to the following format and passed to the API using the app.
role: user
content: question asked
The response from the API is the same.
role: assistant
content: response from the API
Using the two, the collection is built and displayed in a gallery.

Power App setup
ButtonQuestion OnSelect property does the following:
Set the button text
Set the varQuestion variable to the value of TextInputQuestion
Add the question and user role to the colResponses collection
Convert the colResponses collection to JSON
Cal the f Get HTTP flow passing in the collection
with the response from the flow, add the value(s) to the colResponses collection
Reset TextInputQuestion
Set the button text

GalleryRespnses properties:
Visible: If(IsBlank(ThisItem.content),false,true)
Text: ThisItem.content
X: If(ThisItem.role = “assistant”,40,5)
AutoHeight: true

Now for the flow! The flow is triggered from the app and passes a value.
Messages: the new question from the app and previous responses and questions (collection)

The Parse JSON action will take the Messages input and shape the JSON used in the Select Messages action.

The Select Messages action will use the output of the Parse JSON action to help form the correct input for the HTTP action.

Using a Compose action, the model and messages values are set. Note: in the example, I’m using GPT 4 Turbo preview.

The HTTP Request action uses the following values:
Method: Post
URL: https://api.openai.com/v1/chat/completions
Headers:
{
“Content-Type”: “application/json”,
“Authorization”: “Bearer @{outputs(‘Compose_API_Key’)}”
}
Body: output of the Compose Message Body


Another Parse JSON action is used to handle the response from the HTTP Request.


In the Select Response action, the output of Parse JSON Response is used to populate the role and content values.

The final action is to respond to the Power App. The respond to PowerApp step will not work if you use the dynamic content value to populate the response. You must use this expression: outputs(‘Select_Response’)

Again, if data is not sent back to the Power App, chances are you missed the sentence before this one.

As I mentioned at the beginning of this post, this is a basic example. If you see anything wrong with the process or places where it could be streamlined, please let me know.

How do you find ALL the Flows that reference a SharePoint site or list?

I asked this question when I first started down the path of learning about Flow:
How do you find all the Flows running on or referencing a SharePoint list?

Think about it, someone in your company creates a Flow that runs when a SharePoint item is updated. Fast forward a year or so, and that coworker has moved on, and the Flow needs to be updated. If you work for a small company or one that hasn’t fallen in love with Power Platform and Flow, you’re likely in luck, and finding the Flow will take a few minutes. In my case, there are currently 2,712 Flows in my tenant that span several environments.

The PowerShell script I’ve created will query a tenant using the Get-AdminFlow command, return all Flows, and then loop through them. The script can be adjusted to target a single environment using the EnvironmentName parameter. Note: running the script using the Get-Flow action will return all the Flows your AD account can access.

#Install-Module AzureAD
#Install-Module -Name Microsoft.PowerApps.Administration.PowerShell  
#Install-Module -Name Microsoft.PowerApps.PowerShell -AllowClobber 

#connect-AzureAD

function Get-UserFromId($id) {
    try {
        $usr = Get-AzureADUser -ObjectId $id
        return $usr.displayName
    }
    catch {
        return $null
    }
}

#get all flows in the tenant
$adminFlows = Get-AdminFlow 

#set path for output
$Path = "$([Environment]::GetFolderPath('Desktop'))\Flow_Search_for_SharePoint_$(Get-Date -Format "yyyyMMdd_HHmmss").csv"

#set target site
$targetSPSite = "https://yourTenant.sharepoint.com/sites/yourSITE"
$targetSPList = "4f4604d2-fa8f-4bae-850f-4908b4708b07"
$targetSites = @()

foreach ($gFlow in $adminFlows) {

    #check if the flow references the target site
    $refResources = $gFlow.Internal.properties.referencedResources | Where-Object { $_.resource.site -eq $targetSPSite }

    #check if the flow references the target list
    #$refResources = $gFlow.Internal.properties.referencedResources | Where-Object { $_.resource.list -eq $targetSPList }

    if ($refResources -ne $null) {

        #optional - get the user who created the Flow
        $createdBy = Get-UserFromId($gFlow.internal.properties.creator.userId)

        $row = @{}
        $row.Add("EnvironmentName", $gFlow.EnvironmentName)
        $row.Add("Name", $gFlow.DisplayName)
        $row.Add("FlowEnabled", $gFlow.Enabled)
        $row.Add("FlowGUID", $gFlow.FlowName)
        $row.Add("CreatedByUser", $createdBy)
        $row.Add("CreatedDate", $gFlow.CreatedTime)
        $row.Add("LastModifiedDate", $gFlow.lastModifiedTime)
        
        $targetSites += $(new-object psobject -Property $row)
    }
}

#output to csv
$targetSites | Export-Csv -Path $Path -NoTypeInformation

If you don’t want to get the display name of the user who created the Flow, comment out the part of the script that calls the Get-UserFromId function, and you won’t need to connect to Azure.

And to answer my original question: How do you find all the Flows running on or referencing a SharePoint list?
In the script, comment out the part of the script that references $targetSPSite and un-comment $targetSPList. You can get the GUID of the list by navigating to list settings and looking at the URL. Another option is to open the list, view the Page Source, then look for the “listId” property.

In a future post(s), I will outline how to search for all Flows that use different connectors, Dynamics 365 tables (dataverse), triggered from Power Apps, or other objects. All of the info is in the properties of the Flow; getting to it can be a little fun.

How to run a Databricks Notebook using Power Automate

Part of a project I was working on required mashing up some data from SharePoint with data stored in datalake. We settled on creating a Databricks notebook to read an input file, query data lake using the input file, and then export an enriched file.

Here’s a high-level overview of what’s going to be created:

Call the notebook, parse the JSON response, loop until the notebook has finished, then respond to the notebook’s output.

In my case, triggering the notebook will require knowing its URL, bearer token, job id, and input parameters.


Parse the response from the HTTP call:


The notebook will take a little time to spin up, then process the input file. The best way to handle this is to leverage a basic do-until loop to check the status of the notebook job. I opted to use a one-minute delay, call the API to get the job status, parse the response, then evaluate if it’s finished.


One thing to note about the do until action, you don’t want it to run for eternity, and to avoid adding complexity to it, you don’t want to add extra evaluations like: if looped X times, stop
If you expand the Change limits option, you can set how many times it loops or change the duration. Here I’ve set the action to stop looping after 20 tries. For more info on this, please check SPGuides for a detailed overview.

The last step in the flow is to process the response from the notebook. If the job is success(full), get the file from blob storage and load it to SharePoint; otherwise, create a Slack alert.

That’s it; using the example above, you can trigger a Databricks notebook using a Flow.

Authentication
When I set this up, my company allowed the use of Personal Access Tokens (PAT).
https://docs.databricks.com/dev-tools/auth.html#pat
The PAT was then used in the Flow to trigger the notebook.

Use Power Automate to Create Jira Tasks

I’m working on a Power Pages project that requires a Jira service desk task to be created for each portal submission. Out of the box, Jira provides a simple connector to create tasks and requests, but the connect falls short of handling field types other than simple text. This means choice, checkbox, and dropdown fields are not available. This only leaves a couple of options, and I opted to use a simple HTTP action to create the tasks.

Basic overview of what I’ll be creating:
Flow that’s triggered by a dataverse row creation
Create a Jira task and populate metadata
Attach a file to the Jira task

Jira fields and types:
Issue Type – Choice
Request Type – Choice
Tortilla – Choice
Meat – Choice
Veggies – Checkbox multi-select
Number of Tacos – Number
Pickup Date Time – Date and Time
Summary – Text
Attachment – Attachment

Interfacing with the Jira API requires knowing a little about the fields you’ll be updating and the project and issue type you want to use. If you haven’t created one already, you need a Jira API token to work with the API.


Request type:
Go to Project Settings, then look at the URL and copy the value after pid=
https://taco.atlassian.net/secure/project/EditProject!default.jspa?pid=10001

With the ID, you can query the service desk request-types endpoint
https://taco.atlassian.net/rest/servicedesk/1/servicedesk/request/10001/request-types
In the returned payload, note the portal key and key values; combine the two, and you have the request type value tr/9f7c4029-6d23-4cb1-bb8a-02d0050d944b

Project key:
The project key is available on the project settings page, it’s listed under the name field.
Example: TACOS

Issue type:
For simplicity, I’m only dealing with one issue type, and I captured the issueType value using the request-types endpoint noted above.
Example: “issueType”: 10015

For the remaining field values, you can get them in one of two ways.
Create a new issue in the browser, then use the browser developer tools (F12) to inspect each field’s HTML value.

The other option is to click the gear icon (top right), select Issues, click on Custom Fields, search for a field, click on it, click edit detail, then grab the id value from the URL. Once the id is captured, join it with customfield_, resulting in customfield_10073, this is the fields internal value.

In this example, the summary issue type fields are the only ones that does not have a customfield_X naming convention. It might be possible that some system-generated fields have a diffident naming convention, but I’ll dig into that another day.

Column Display NameColumn Internal NameColumn Type
Issue Typeissuetypesystem
Request Typecustomfield_10010system
Tortillacustomfield_10073Select List (single)
Meatcustomfield_10074Select List (single)
Veggiescustomfield_10075Checkboxes
Number of Tacoscustomfield_10076Number Field
Pickup Date Timecustomfield_10077Date Time Picker
Summarysummarysystem

Endpoint URL:
https://taco.atlassian.net/rest/api/3/issue/
Headers: {“Content-Type”: “application/json”}
Authentication: Raw
Key: Basic aWhddsfadfafa..NOT…A…REAL…KEY..dafdfdafd=
Example payload:

{
  "fields": {
    "project": {
      "key": "TACOS"
    },
    "customfield_10010": "tr/9f7c4029-6d23-4cb1-bb8a-02d0050d944b",
    "summary": "Taco order summary",
    "issuetype": {
      "id": "10015"
    },
    "customfield_10073": {"value": "Flour"},
    "customfield_10074": {"value": "Chicken"},
    "customfield_10075": [{"value": "Pico"},{"value": "Grilled Veggies"}],
    "customfield_10076": 2,
    "customfield_10077":"2022-11-05T11:05:00.000+0000"
  }
}

View of the task in Jira

How do you attach a file to a Jira task using Power Automate?
Attaching a file to a Jira task requires one more API call and it’s simple!

Endpoint URL:
https://taco.atlassian.net/rest/api/3/issue/Key/attachments
Headers: {“X-Atlassian-Token”: “no-check”}
Authentication: Raw
Key: Basic aWhddsfadfafa..NOT…A…REAL…KEY..dafdfdafd=

Example payload:

{
  "$content-type": "multipart/form-data",
  "$multipart": [
    {
      "headers": {
        "Content-Disposition": "form-data; name=\"file\"; filename=@{outputs('Get_file_properties')?['body/{FilenameWithExtension}']}"
      },
      "body": @{body('Get_file_content')}
    }
  ]
}

For the attachment, I’m getting a file from SharePoint and passing its contents to the API call. The same thing works with Azure blob storage or grabbing file from dataverse. If you want to attachment more than one file, create additional HTTP attachment calls.

Here’s a simple overview of the Flow:

Parse JSON schema:

{
    "type": "object",
    "properties": {
        "id": {
            "type": "string"
        },
        "key": {
            "type": "string"
        },
        "self": {
            "type": "string"
        }
    }
}

The Jira documentation is great and their forums are active and helpful.

Power App Unable to add flow

I was in the process of porting a production Power App to a dev environment, and I ran into this error.

Unable to add flow
There was a problem adding your service. Please try again later.

The problem was that I tried to add an existing Flow to my Power App, but it was turned off.



Navigate to make.powerapps.com, locate the Flow you are trying to add to the Power App, turn it on, then try adding to the app.

Create Dynamic Hyperlinks And Send An Email Action

Over the years, some updates to Flow have been better than others, and others, not so much. If memory serves, the send an email action would use dynamic hyperlinks without much work, but something went sideways with one of the updates causing dynamic hyperlinks not to work as you’d want.

Here is a basic example of including a hyperlink in an outgoing email; further down the page, I’ll provide a more realistitc example.


In this example, I setup the Flow to trigger when an item is added to a SharePoint library. The key thing to note in this example is the value of the varHyerplink variable; note the double quotes around the link to item.

Use Power Automate to Update a SharePoint Person Field

Using the SharePoint HTTP flow action to update a person or group field, I kept getting this error:

A 'PrimitiveValue' node with non-null value was found when trying to read the value of a navigation property; however, a 'StartArray' node, a 'StartObject' node, or a 'PrimitiveValue' node with null value was expected.

The field I was attempting to update is named Submitted By, with an internal name of Submitted_x0020_By. Each time I tried to update the field I was seeing the error noted above. It wasn’t until I looked at one of my previous flow runs did I notice what the issue was. It turns out, that the field name I should be using is Submitted_x0020_ById.



Update flow:

How do you update a Person field if the field allows for multiple selections? The example below will update the field with two different user values, but clearly, this could be extended to be more dynamic.

body('Send_an_HTTP_request_to_SharePoint_User_1')?['d']?['Id']
body('Send_an_HTTP_request_to_SharePoint_User_2')?['d']?['Id']

concat('[',outputs('Compose_1'),',',outputs('Compose_2'),']
{
    "__metadata": {
        "type":"SP.Data.AssignedToListListItem"
    },
    "SubmittedByIDsId": {
         "results": [
                 6,
                 54
          ]
    }
}

Power Automate Convert Time Zone Error

I was in the middle of streamlining an old flow and needed to compare two dates and times. To simplify things, I opted to use the Convert time zone flow action, but it gave me an error that seemed a little odd.

Unable to process template language expressions for action ‘Convert_time_zone’ at line ‘0’ and column ‘0’: ‘The template language function ‘convert timezone’ expects its fourth parameter to be a string that contains a date time format. The provided value is of type ‘Null’. Please see https://aka.ms/logicexpressions#ConvertTimeZone for usage details.’.

Notice that I did not select an option for the Format string field, this is required, but there’s not a red * next to the field.



Select an option for that field, and the universe will be back in alignment.



Add Company Holidays to Your Outlook Calendar Using Power Automate

Below are all the steps required to add your company holidays to your Outlook calendar. This is a simple approach that can easily be updated to pull the holidays and dates from other data sources like Exel, SharePoint, Dataverse…

Overview of the flow that will be created:

All of the holidays (events) will be stored in an array using a compose action.

[
{"Event":"Holiday - New Years","Date":"01/01/2022"},
{"Event":"Holiday - MLK Day","Date":"01/17/2022"},
{"Event":"Holiday - Presidents Day","Date":"02/21/2022"},
{"Event":"Holiday - Memorial Day","Date":"05/30/2022"},
{"Event":"Holiday - Juneteenth Day","Date":"06/20/2022"},
{"Event":"Holiday - Independence Day","Date":"07/04/2022"},
{"Event":"Holiday - Labor Day","Date":"09/05/2022"},
{"Event":"Holiday - Thanksgiving Day","Date":"11/24/2022"},
{"Event":"Holiday - Day After Thanksgiving","Date":"11/25/2022"},
{"Event":"Holiday - Christmas Day","Date":"12/26/2022"}
]

After that, a parse JSON event will be used to make the events available in the apply to each action. Note: after the parse JSON action is added to the flow, click the generate from sample button and paste in the holidays JSON from the step above.

Next, add an apply to each action to the flow. In the action, the output box will hold the body of the parse JSON action.



The convert time zone action is used to standardize the date format. For the base time use the Date field from the parse JSON action.

The last item that needs to be added is the outlook create event action. Note: for the End time property a formula is used: addDays(body(‘Convert_time_zone’),1)






Save the flow and run it.

Edit/Update –
For the End time value, you can input this expression: addHours(body(‘Convert_time_zone’),23) . If you set Is all day event to Yes, you’ll need a timespan that covers most if not all of the day.
Also, if you want to set Is reminder on, it must be input as minutes, so 17 hours would be 1020 minutes.


Flow Error Unable to process template language expressions for action

Unable to process template language expressions for action 'Apply_to_each_sftp_file' at line '1' and column '30517': 'The template language expression 'body('List_files_in_folder')?['body']' cannot be evaluated because property 'body' cannot be selected. Array elements can only be selected using an integer index. Please see https://aka.ms/logicexpressions for usage details.'.

From one day to the next one of my flows stopped working with the error above. The flow is super simple:
Get files from an SFTP folder
Loop through the files
Copy file to another location
Delete file

Looking at the apply to each step I noticed body element was referenced. This might have been related to how I set up the loop or the autogenerated action.

Action the flow was unhappy with:

Updated the flow and it started working:

Note how the ?[‘body’] element is missing from the second picture.

Fix?
Created a new Apply to each loop by first setting a variable to the name of the file I’m looping on, then added my other actions. There might be a better way to fix this, but for now, this is what works.