When building enterprise solutions in SharePoint Framework, long-running processes are common:
triggering Power Automate flows
importing files
processing lists
migrating content
generating reports
synchronizing data
Building Real Progress Tracking in SPFx with PnP Progress, PnPjs, and Power Automate
When building enterprise solutions in SharePoint Framework, long-running processes are common:
- importing files
- processing lists
- migrating content
- generating reports
- synchronizing data
- triggering Power Automate flows
One of the biggest UX problems is lack of visibility.
The user clicks a button…
…and nothing happens.
That is where the PnP Progress Control becomes extremely useful.
This article explores:
- the real architecture of the control
- how actions work internally
- how to integrate it with PnPjs
- how to integrate it with Power Automate
- how to build a production-grade tracking model
Understanding the Progress Control
The Progress control is not a percentage bar.
This is the first thing many developers misunderstand.
It is a step-based process tracker.
Instead of:
40%
it thinks in:
Step 1Step 2Step 3Step 4
Its main model:
<Progress actions={actions} currentActionIndex={currentActionIndex}/>
The UI calculates progress based on action position.
Formula:
currentActionIndex / actions.length
Example:
4 actionscurrentActionIndex = 2
Result:
50%
The Real Action Model
Official docs simplify this.
But internally the real interface is:
export interface IProgressAction { title: string; subActionsTitles?: string[]; hasError?: boolean; errorMessage?: string;}
This gives you:
title
Main action label:
{ title: "Reading SharePoint list"}
subActionsTitles
Nested sub-steps:
{ title: "Processing items", subActionsTitles: [ "Validating data", "Calculating totals", "Saving metadata" ]}
Important:
These are visual only.
They do not execute.
hasError
Marks step as failed:
{ title: "Saving data", hasError: true}
errorMessage
Detailed error:
{ title: "Saving data", hasError: true, errorMessage: "Unable to save the item."}
How State Works
This is how the control determines state:
index < currentActionIndex => completedindex = currentActionIndex => runningindex > currentActionIndex => pendinghasError = true => failed
Visual:
✔ Connect⏳ Read List○ Process○ Save
Mock Testing Strategy
Before production:
Always mock.
Example:
const actions = [ { title: "Connect" }, { title: "Read" }, { title: "Process" }, { title: "Save" }];
Then simulate:
for (let i = 0; i <= actions.length; i++) { setCurrentActionIndex(i);}
This tests:
- normal flow
- completed state
- pending state
- long running
- errors
- sub actions
Mock first.
Production later.
Integrating with PnPjs
This is where Progress becomes powerful.
Architecture:
SPFx UI↓Progress control↓PnPjs executes operations↓UI updates state
Example:
const sp = getSP(context);setCurrentActionIndex(0);const list = await sp.web.lists.getByTitle("Documents")();setCurrentActionIndex(1);const items = await sp.web.lists .getByTitle("Documents") .items();setCurrentActionIndex(2);for (const item of items) { console.log(item.Title);}setCurrentActionIndex(3);
Mapping:
| PnPjs Operation | Progress Step |
|---|---|
| Connect SP | Step 1 |
| Read List | Step 2 |
| Load Items | Step 3 |
| Process Data | Step 4 |
Perfect match.
Real Batch Processing Pattern
Very common:
const items = await sp.web.lists .getByTitle("Tasks") .items();
Then:
for (const item of items) { await sp.web.lists .getByTitle("Tasks") .items .getById(item.Id) .update({ Status: "Done" });}
Progress:
✔ Loading tasks⏳ Updating tasks○ Finalizing
This is one of the best uses.
Integrating with Power Automate
Can Progress track Power Automate?
Yes.
But indirectly.
Architecture:
SPFx↓Trigger Flow↓Flow executes↓SPFx polls status↓Progress updates
There are 4 models.
Model 1 — Fire and Forget
SPFx triggers Flow:
await fetch(flowUrl, { method: "POST"});
Progress:
✔ Sent⏳ Running✔ Done
Simple.
But no internal tracking.
Model 2 — Polling
Flow returns execution ID.
SPFx polls:
setInterval(async () => { const result = await checkStatus();}, 3000);
Better.
Model 3 — SharePoint Status List (Best Pattern)
This is the strongest architecture.
Flow writes into SharePoint.
List:
ExecutionIdCurrentStepStatusPercentMessage
Example:
| Step | Status |
|---|---|
| Download Files | Completed |
| Parse Data | Running |
| Save Results | Pending |
SPFx reads:
const status = await sp.web.lists .getByTitle("FlowProgress") .items();
Builds actions:
const actions = status.map(item => ({ title: item.Title, hasError: item.Status === "Error", errorMessage: item.Message}));
This is the enterprise pattern.
Advantages:
- historical log
- debugging
- audit
- resumability
- visibility
Best option.
Recommended Enterprise Architecture
Best production model:
User clicks button↓SPFx triggers Flow↓Flow creates status rows↓Flow updates current step↓SPFx polls list via PnPjs↓Progress control renders
This combines:
SharePoint Framework
+
PnPjs
+
Power Automate
+
PnP Progress Control
This is the most scalable architecture.
When to Use This Pattern
Use it when:
✔ batch updates
✔ migrations
✔ file processing
✔ Power Automate orchestration
✔ long API calls
✔ document generation
✔ compliance jobs
✔ imports/export
Avoid for:
✖ simple button clicks
✖ tiny CRUD operations
✖ single-item updates
Final Recommendation
Think of it like this:
Progress does not execute.
Progress tells a story.
PnPjs does the work.
Power Automate does the orchestration.
SharePoint stores the truth.
That separation is what makes this architecture powerful.
