A lengthy career working as a SharePoint developer, admin, and architect. I'm now working in the Power Platform and Azure spaces.
What happened to InfoPath?
I’m working on a simple report to pull some data from Dataverse into a Power BI report. The data includes some choice fields, and when I first generated the report, I only retrieved the internal value of the fields. So my Status field values look something like this: 10000001 Cleary, no one wants to see this, and I needed to grab the display values: Ordered, Processing, Shipped
Part of the problem was related to using Native Query to pull in the data and not selecting the correct field. Meaning there are two or more columns for each choice field. My query looked something like this: Select title, status, createdon from orders where customerid = ‘875-6309’
The query should have been: Select title, statusname, createdon from orders where customerid = ‘875-6309’
tl;dr try placing the word ‘name’ directly after your choice field name. status would be statusname state would be statename
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?
UPDATE / EDIT – READ THIS Part Before you start on this, please ensure that your account or the account you are using to run the script has sufficient permissions to the target environment(s).
If you run that command and look at the returned properties and see an error, that means you do not have the correct permissions to move forward. You can check your permissions in the Power Platform admin center: https://admin.powerplatform.microsoft.com/
/end of update
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.
Last year, my team rolled out a Power App Portal (Power Pages) to allow customers to submit requests with attachments. The attachments are stored in Azure Blob Storage, and we use Cloud Mersive to virus scan the submitted attachments. Not to get too deep into the weeds, the process flows like this: Attachment is uploaded –> lands in the Dataverse Note (annotation) table –> then is shipped to blob storage
Now the problem: users can name a file whatever they like and upload them. This quickly became an issue due to Flow not always being able to find the blobs associated with the request if the filename contained some special characters. Example: MyTrademark®.pdf
There are some great examples online for replacing special characters with a space or another supported character, but I wanted to take a different approach that seemed a lot more efficient to me. Where my example differs is the use of the Filter Array Flow action to only check the characters of the filename, as opposed to looping through each letter of the alphabet and comparing it to each letter in the filename.
Here’s the completed Flow, but I’ll dig into each step in this post.
The Compose Chars action holds the array of characters I will use to validate the characters in the supplied filename. This can be shortened if the input filename is to be set to uppercase or lowercase; only one set of the alphabet is needed.
Compose Org Filename:string('my super 123 longer $%^&^ file /// name ^^^ with junk in it.xlsx') Compose Split Extension:last(split(outputs('Compose_Org_Filename'), '.')) Compose Concat Extension:concat('.', outputs('Compose_Split_Extension')) Compose Get Filename:split(outputs('Compose_Org_Filename'), outputs('Compose_Concat_Extension'))[0]
The point of the Apply to each loop is to iterate over each item in the filename. Note: I’m using a Chunk function to break apart the filename. I first tried using a Spilt function, but there would be no end to what the delimiter might be.
Apply to each:chunk(outputs('Compose_Get_Filename'),1)
Filter array Chars: From: Compose Chars char is equal to Current item Here is the advanced view of the action: @equals(item()?['char'], items('Apply_to_each')) If you think of it like a SQL statement, it would be: Select * from Compose Chars Where Char = Current item The filter checks if the current item in the apply to each loop is in the Compose Chars array.
Condition: empty(body('Filter_array_Chars')) is equal to true If the current item is not in the array, skip it (yes), else start building the filename (no)
This hack is needed due to not being able to set a Flow action equal to itself. Think of it like a programmatic iteration. i++ or i = i + 1
Compose Temp is a placeholder for the varNameBuilder variable. Set variable Name Builder:concat(outputs('Compose_Temp'),items('Apply_to_each'))
Things to consider: Empty filename – What if the filename is nothing but special / unwanted characters? At the end of the Flow, you’d want to use a Length function to check varNameBuilder to see if it’s greater than X. example: !@#@$#$%%^&.pdf The result from the Flow would be .pdf, and updating the filename would fail. To my knowledge, you can’t name a file like that, but you get the point.
Also, I’ve had users upload files with non-English characters, so there is a viable chance that someone, at some point, might upload a file like this: 我喜欢炸玉米饼.pdf
Making the Flow available to other flows – If the Flow is created in a solution, it could be used more like a function, and other Flows in the solution could reference it. This would be a great example of a reusable Child Flow.
How do you hide a field on a PowerApp when opening a new form? The approach below uses a single screen form instead of multiple screens for the various forms.
I started by creating a new SharePoint list and added two text fields: Not on New Form On New Form Using the customize form option, I entered the Power App designer.
When the PowerApp designer opens, it will look like this:
To help see what’s going on with the form mode, add a text label to the form and set its Text property to: "Form Mode: " & Text(SharePointForm1.Mode)
Select the field (Data Card) that should not appear on the new item form, then select the Visible property. For the Visible property, enter the following: If(SharePointForm1.Mode = 1, false, true) . If your SharePointForm1 is named something else, use it instead of the value I presented.
Breaking down the formula a little: If the SharePoint form mode is equal to 1, visible should be false, else true.
Save and publish the app, then check if it’s functional as planned.
I’m working with the Dataverse Web API and ran into this error while trying to write to a table. Invoke-RestMethod : {"error":{"code":"0x8006088a","message":"Resource not found for the segment 'table name'."}}
The fix is to use the plural name of the table, but sometimes my engrish ain’t the bestest and I was struggling to figure out what the plural of Taco Order was (joking). If you want to find all the tables in your environment quickly, you can toss the API URL into a browser, which will list all the plural table names.
Microsoft Power Automate (Flow) has a hard time with files over 1GB, and depending on the business process; this can cause a lot of headaches. Due to this, I opted to use an Azure runbook to download files, then use a Flow to handle downstream processing. The scope of this post is to show how to download files from an SFTP and move them to blob storage.
Tooling being used: Azure Resource Group Azure Automation Account Posh-SSH Module Azure Runbook (PowerShell) + Hybrid Worker Azure Storage Account
Overview of what the script does: Connect to Azure Connect to the SFTP server Get a list of files from the server where the size is greater than 0 Loop through the files Create a temp folder Download the file from SFTP to the temp folder Create a new blob using the file from the temp folder Check if the blob was created If yes – remove the associated file from the SFTP server Clean up the SFTP session
#sftp items
$port = "22"
$Password = "my password"
$User = "sftp-user"
$hostName = "sftp.sharepointed.com"
$SftpPath = '/sftp_home/out/bigfiles/'
#blob storage related variables
$container = "mycontainer"
$blobFilePath = "myfolder/newfiles"
#get connection setup
$account = Connect-AzAccount -Identity -Confirm:$false | Out-Null
write-output $account
try {
$accKey = (Get-AzStorageAccountKey -ResourceGroupName "my rg name" -Name "my value")[0].Value
$context_storageAcct = New-AzStorageContext -StorageAccountName "my value" -StorageAccountKey $accKey
write-output $context_storageAcct
}
catch {
$errors += [PSCustomObject]@{Item = "azcontext"; Error = $_.Exception }
write-output "error get AZcontext"
}
#sftp setup
$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force
$Credentials = New-Object System.Management.Automation.PSCredential($User, $secpasswd)
write-output "====== Starting SFTP Session on $($hostName)"
$ssh = New-SFTPSession -ComputerName $hostName -Credential $Credentials -Port $port -Force -Verbose
write-output " +++ SFTP Session started on $($hostName)"
#get files from sftp
$listOfFiles = Get-SFTPChildItem -SessionId $ssh.SessionId -Path $SftpPath
$filteredFiles = @($listOfFiles | Where-Object { $_.Attributes.Size -gt 0 })
foreach ($file in $filteredFiles) {
try {
$blobPath = $($blobFilePath + "/" + $file.Name)
$localFolderBase = [System.Io.Path]::GetFileNameWithoutExtension($file.Name)
$localFolder = $($env:temp + "\" + $localFolderBase)
$fileToCopy = $($localFolder + "\" + $file.Name)
#create new folder to hold the downloaded file
New-Item -Path $localFolder -ItemType Directory -Force -Confirm:$false | Out-Null
#download file to local storage
Get-SFTPFile -SessionId $ssh.SessionId -RemoteFile $file.FullName -LocalPath $localFolder
#move file to container
Set-AzStorageBlobContent -File $fileToCopy -Container $container -Blob $blobPath -StandardBlobTier Cool -Context $context_storageAcct.Context -Force -Confirm:$false
#check if file was saved to the container
$blob = Get-AzStorageBlob -Blob $blobPath -Container $container -Context $context_storageAcct.Context -ErrorAction Ignore
if ($blob)
{
Write-output "Blob can be removed from SFTP"
Remove-SFTPItem -SessionId $ssh.SessionId -path $file.FullName -Force
}
}
catch {
write-output $_.Exception
}
}
#terminate the SFTP session
Remove-SFTPSession -SessionId $ssh.SessionID
The script can easily be expanded to trigger another runbook or move the files to SharePoint. One thing to note, my runbooks run on a Hybrid Worker, and this helps a lot with long-running jobs or jobs that are memory intensive.
My Power App Portal (Power Pages) environments are configured to use Azure blob storage for form attachments. One of the primary reasons for doing this is to avoid filling up expensive dataverse storage with endless attachments submitted by enduers.
This article outlines how to set up Azure storage: link
What I’m going to demo is how to get ONE attachment that’s uploaded to a form. If your form allows multiple attachments, you’d simply loop through them.
In the example, I’m using the soon-to-be-obsolete dataverse connector, but the same basic flow design applies to the normal connector.
When a row is added to my table, the flow is triggered. The flow then queries the Note (annotation) table using the ID from the source table. filter query: (_objetid_value eq souce_table_id)
The list rows notes query will result in an array being returned, but I’m only dealing with one attachment, so there’s no need to loop through it. To avoid an unnecessary loop, a function can be used to target a single object from the array: first(body(‘List_rows_Notes’)?[‘value’])?[‘annotationid’]
From the Get row note action, annotationid and filename will be needed to help form the path to the blob. Using the concat function I’m combing the container name, annotationid, and filename. Also, note the transformation on annotationid, the hyphens need to be removed, and the string needs to be lowercase. The last part of the transformation is to remove .azure.txt from the filename.
The end result of the transformation will be: /blobcontainer/annotationid/filename /blobcontainer/cf03e4cf7f72ad118561002248881923/example.pdf
With the path to the blob formed, the get blob content action can retrieve the file.
It’s that simple.
A couple of notes: It would be wise to leverage a virus-scanning tool like Cloudmersive. If you haven’t already noticed, when a user uploads a file that contains special characters in the name…it’s saved to the Note table without the special characters, but when it’s moved to blob storage, the characters will be in the name. Yes, that’s a bug Microsoft has yet to fix. You can avoid this by adding Javascript to the upload page to block files that fall into this category. OR. Write another flow to clean file names before the form is processed. Example: Uploaded filename: my report 1:2:3.pdf Note table: my report 123.pdf Blob: my report 1:2:3.pdf
Currently working on a project, and my UX team asked if it was possible to change the look of the B2C sign-up / password change page to include visual hints to meet the password complexity requirements. We’ve all seen it before, you visit a site where you need to sign up, and the password needs to be X characters long and contain this and that, but some sites include a cute visual to help identify what requirements have been met.
If you read the B2C documentation, it’s strongly noted not to use JS libraries outside of the libraries native to B2C. I opted to keep my solution as simple as possible to avoid additional security gaps.
To get this working, I followed the steps outlined in link 1. There I created all of the needed assets in the Portal Management section of the Power Pages environment. Next, I used the content from link 3 to update the Web Template that I created in the previous step. After that, I updated the Web Template to include the div noted in link 4; this is extremely important and can’t be skipped. The last part of the process is to update the B2C user flow policy to reference the page created in step 1.
Here is a copy of my Web Template file from Portal Management.
<!DOCTYPE html>
<html>
<head>
<style>
/* Style all input fields */
input {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-top: 6px;
margin-bottom: 16px;
}
/* Style the submit button */
input[type="submit"] {
background-color: #04aa6d;
color: white;
}
/* Style the container for inputs */
.container {
background-color: #f1f1f1;
padding: 20px;
}
/* The message box is shown when the user clicks on the password field */
#message {
display: none;
background: #f1f1f1;
color: #000;
position: relative;
padding: 20px;
margin-top: 10px;
}
#message p {
padding: 10px 35px;
font-size: 18px;
}
/* Add a green text color and a checkmark when the requirements are right */
.valid {
color: green;
}
.valid:before {
position: relative;
left: -35px;
content: "✔";
}
/* Add a red text color and an "x" when the requirements are wrong */
.invalid {
color: red;
}
.invalid:before {
position: relative;
left: -35px;
content: "✖";
}
</style>
</head>
<body>
<!--this div is the most important part of the process-->
<div id="api"></div>
<div id="message">
<h3>Password must contain the following:</h3>
<p id="letter" class="invalid">A <b>lowercase</b> letter</p>
<p id="capital" class="invalid">A <b>capital (uppercase)</b> letter</p>
<p id="number" class="invalid">A <b>number</b></p>
<p id="length" class="invalid">Minimum <b>8 characters</b></p>
</div>
<script>
var myInput = document.getElementById("password");
var letter = document.getElementById("letter");
var capital = document.getElementById("capital");
var number = document.getElementById("number");
var length = document.getElementById("length");
// When the user clicks on the password field, show the message box
myInput.onfocus = function () {
document.getElementById("message").style.display = "block";
};
// When the user clicks outside of the password field, hide the message box
myInput.onblur = function () {
document.getElementById("message").style.display = "none";
};
// When the user starts to type something inside the password field
myInput.onkeyup = function () {
// Validate lowercase letters
var lowerCaseLetters = /[a-z]/g;
if (myInput.value.match(lowerCaseLetters)) {
letter.classList.remove("invalid");
letter.classList.add("valid");
} else {
letter.classList.remove("valid");
letter.classList.add("invalid");
}
// Validate capital letters
var upperCaseLetters = /[A-Z]/g;
if (myInput.value.match(upperCaseLetters)) {
capital.classList.remove("invalid");
capital.classList.add("valid");
} else {
capital.classList.remove("valid");
capital.classList.add("invalid");
}
// Validate numbers
var numbers = /[0-9]/g;
if (myInput.value.match(numbers)) {
number.classList.remove("invalid");
number.classList.add("valid");
} else {
number.classList.remove("valid");
number.classList.add("invalid");
}
// Validate length
if (myInput.value.length >= 8) {
length.classList.remove("invalid");
length.classList.add("valid");
} else {
length.classList.remove("valid");
length.classList.add("invalid");
}
};
</script>
</body>
</html>
The idea behind this was to keep it as simple as possible and to get a basic example created. Yes, you can store the file in blob storage, but I wanted to keep all portal parts close together and avoid added complexity. (not that creating this page in Portal Management was easy)
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.
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, 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 or Ctrl + Shift + I) 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, and then grab the ID value from the URL. Once the ID is captured, join it with customfield_, resulting in customfield_10073, which is the field’s internal value.
In this example, the summary issue type fields are the only ones that do not have a customfield_X naming convention. It might be possible that some system-generated fields have a different naming convention, but I’ll dig into that another day.
Column Display Name
Column Internal Name
Column Type
Issue Type
issuetype
system
Request Type
customfield_10010
system
Tortilla
customfield_10073
Select List (single)
Meat
customfield_10074
Select List (single)
Veggies
customfield_10075
Checkboxes
Number of Tacos
customfield_10076
Number Field
Pickup Date Time
customfield_10077
Date Time Picker
Summary
summary
system
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:
Attachment I’m getting a file from SharePoint and passing its contents to the API call for the attachment. The same thing works with Azure blob storage or grabbing a file from the dataverse. If you want to attach more than one file, create additional HTTP attachment calls.