
A few weeks back, I ran into one of those Power Apps walls that shouldn’t exist but does. I was building a Canvas App for a client. It was nothing fancy, I just needed a clean interface for uploading and downloading Excel and CSV files as part of a data pipeline. Power Apps felt like the obvious choice: quick to build, low barrier, integrates well with the rest of the Microsoft stack.
Then I tried to add a file picker.
Power Apps handles images, audio, and video natively. Drop in a media component, wire it up, done. Files like Excel and CSV? Nowhere near as smooth. The image/media component simply won’t accept them. There’s no native “file picker” control. And if you start Googling, the suggested workaround is often Dataverse file storag, which works, but comes at a price that makes it a unsuited for anything beyond small-scale use.
In this post I’ll walk through the workaround I landed on: Azure Blob Storage as the actual file storage backend, a lightweight Dataverse table as an index, two Power Automate flows for upload and download, and one non-obvious trick inside Canvas App to get file input working. Let’s get into it!
Why not just use Dataverse file storage?
It’s the first thing you’ll read about. Dataverse supports file columns, it integrates cleanly with Power Apps, and it feels like the obvious answer.
The problem is cost. Dataverse database storage runs at $40/GB/month. Even Dataverse file storage (the cheaper tier) comes in at around $2/GB/month. If you’re dealing with data files that accumulate over time, that bill compounds fast.
Azure Blob Storage, by comparison, costs a fraction of a cent per GB. For a data professional building internal tooling, the math isn’t close.
I also want to note that SharePoint is another valid option here. It stores files cheaply and integrates well with Power Platform. But this post focuses on Azure Storage, which gives you more control and is probably the better fit if you’re already running data infrastructure on Azure.
The architecture
Before getting into the steps, here’s the full picture:
-
Azure Storage Account where the actual files live
-
Dataverse
Filestable a lightweight index that tracks file metadata (storage account name and path), making files easy to query from within Power Apps -
upload_fileflow takes a file from Power Apps, pushes it to Blob Storage, writes a row to Dataverse -
download_fileflow takes a file ID, looks up the path in Dataverse, generates a SAS URL, returns it to Power Apps for launch -
Canvas Appwhich serves as the interface, with a hidden DataCard for file input (more on why in a moment), an upload button, and a gallery of uploaded files with download and delete actions per row
Let’s build it.
Step 1: Set up Azure Storage
Create a Storage Account in your Azure subscription and add a container to hold the uploaded files. Give it a sensible name, something like powerapp-files. Note the storage account name and generate a connection string or SAS token; you’ll need it when configuring the Power Automate connector.
No special configuration required beyond the basics. Private access on the container is fine, the download flow will generate time-limited SAS URLs so files can be opened without making the container public.
Step 2: Create the Dataverse Files table
In Power Apps, create a new Dataverse table called Files. Add two custom columns:
-
Storage Account Name (text) — the name of the Azure Storage Account
-
Path (text) — the blob path within the container (e.g.
powerapp-files/report-2024-q4.xlsx)
That’s it. This table isn’t the file store, but the index. It gives Power Apps a clean, queryable list of what’s been uploaded, without having to call Azure Storage directly every time the gallery loads. When a file is uploaded, a row gets added here. When it’s deleted, the row gets removed.
Step 3: The file input trick in Canvas App
This was the missing puzzle piece for me, that I didn’t know upfront.
Power Apps has an Add picture control (the image drop component). It looks like what you want. It isn’t, since it only accepts image files. For Excel, CSV, PDF, or anything else, it doesn’t accept the file.
The way to get a proper file picker that accepts any file type is to use the Attachments control from a SharePoint list DataCard. Here’s how to set it up:
-
In your Canvas App, insert a SharePoint list data source. It doesn’t matter which list — you’re just using it to get access to the Attachments control. Use any SharePoint list you have available.
-
Insert a Edit form connected to that SharePoint list.
-
Inside the form, find the Attachments data card. This is the component you need, pull this out of the parent component.
Name this DataCard something clear (like DataCard). The DataCard.Attachments property is what the upload button will read from.
Step 4: Build the upload_file flow
In Power Automate, create a new instant cloud flow triggered by Power Apps. Name it upload_file.
The flow expects a single input from Power Apps:
file (object)
- contentBytes (string, base64)
- name (string)
The steps inside the flow:
1. Power Apps trigger: receives the file input.
**2. Create Block Blob (V2):**Azure Blob Storage connector. Set the storage account name, container name, and blob name (use the name from the input). Pass contentBytes as the blob content.
**3. Add a new row:**Dataverse connector. Add a row to your Files table with:
-
Storage Account Name: your account name
-
Path:
your-container/+ the file name
**4. Respond to Power Apps:**return { "success": true }.
Step 5: Wire up the upload button in Canvas App
Back in the Canvas App, add a Button outside the DataCard and label it “Upload”. Set its OnSelect to:
Set(
varUploadDone,
ForAll(
DataCard.Attachments,
upload_file.Run({
file: {
contentBytes: Value,
name: Name
}
})
)
);
Refresh(Files)
What this does: it iterates over everything attached in the DataCard, calls upload_file for each file, and once done, refreshes the Files table so the gallery updates immediately.
DataCard.Attachments returns a collection where each item has Value (the base64-encoded content) and Name (the filename). That maps directly onto what the flow expects.
Step 6: Build the download_file flow
Create another instant cloud flow triggered by Power Apps. Name it download_file.
Input:
file_id (string) — the Dataverse row ID
Steps:
1. Power Apps trigger: receives the file ID.
**2. Get a row by ID:**Dataverse connector. Look up the row in your Files table using file_id. This gives you the Storage Account Name and Path.
**3. Create SAS URI by Path (V2):**Azure Blob Storage connector. Pass in the storage account name and the path from the Dataverse row. Set an expiry (a few minutes is fine for a download link).
4. Respond to Power Appsreturn { "result": <SAS URI> }.
Good to know: The SAS URI is a time-limited, direct download link. You don’t need public access on the container.
Step 7: Build the file gallery in Canvas App
Add a Vertical Gallery to the Canvas App and set its data source to the Files Dataverse table. Each row in the gallery represents one uploaded file.
Inside each gallery row, add two buttons: Download and Delete.
Download button OnSelect:
Set(
varDownloadResult,
'download_file'.Run(ThisItem.File)
);
Launch(varDownloadResult.result, {}, LaunchTarget.New)
This calls the flow with the file’s Dataverse row ID, gets back the SAS URL, and opens it in a new tab. The browser handles the download from there — the file streams directly from Azure Storage.
Delete button OnSelect:
The delete is straightforward. Use the Dataverse Remove function to delete the row, then call the Azure Blob Storage connector via a delete_file flow (same pattern: receive file ID, look up path, delete blob, remove Dataverse row). Refresh the gallery after.
What you’ve built
At this point you have:
-
A working file picker in Power Apps that accepts any file type
-
Files stored cheaply in Azure Blob Storage, not Dataverse
-
A clean Dataverse index that makes the gallery fast and queryable
-
Time-limited SAS URLs for downloads, so nothing is publicly exposed
-
A pattern that scales: add more file types, validations, or metadata columns to the Dataverse table as needed
The main cost driver here is Azure Blob Storage, which for typical internal tooling usage will run you a few cents a month rather than several dollars per GB.
Where to take it further
A few obvious extensions if you need them:
**File type validation:**check Name in the upload button before calling the flow. Reject anything that isn’t .xlsx, .csv, or whatever your use case requires.
**User attribution:**add a Uploaded By column to the Dataverse table and pass User().Email from Power Apps into the upload flow.
**Multi-environment setup:**parameterise the storage account name and container in the flow so you can reuse it across dev and prod environments without duplicating the flow.
**Larger files:**Power Apps has a file size limit on attachments (by default 10MB per attachment). For larger files, you’ll need a different ingestion path, a custom API or Azure Function is the usual answer.
The architecture here is deliberately minimal. Get it working end-to-end first, then add the controls you actually need for your use case.
Hi, I’m Bastiaan 👋🏼 Founder of datalyft, a small Dutch company helping companies transform their raw data into real value. I write about the Modern Data Workflow, where I explore tools & processes to supercharge your data capabilities. Follow me for more!