Skip to main content

Build a Career Development Plan App in Power Apps — Part 5: Build the My Plan Screen

· 27 min read
Jagdish Kumawat
Founder @ Dewiride

People open the Career Development Plan app to see what's expected of them. This part builds that screen: My plan lists the competencies for the person's job title, each with its level and a badge saying where their response stands. It also explains how the app asks SharePoint for exactly those items, and nothing more.

This is Part 5 of a 14-part series.

  1. Part 1: Plan the App and Set Up the Demo Organization
  2. Part 2: Create the SharePoint Site and Lists
  3. Part 3: Create the Solution, Environment Variables and Canvas App
  4. Part 4: Build the Home Screen
  5. Part 5: Build the My Plan Screen (you are here)
  6. Part 6: Build the Response Screen
  7. Part 7: Share the App and Build the My Team Screens
  8. Part 8: Build the Review Screen for Manager Approvals
  9. Part 9: Let Managers Manage Competencies
  10. Part 10: Test Every Role, Joiners and Leavers (coming soon)
  11. Part 11: Build a Setup Flow That Creates the SharePoint Lists (coming soon)
  12. Part 12: Run the Solution Checker and Export the Solutions (coming soon)
  13. Part 13: Install the App in a New Environment (coming soon)
  14. Part 14: Recap, Production Checklist and Next Steps (coming soon)

Part 4 built the Home screen and its My plan button. Now the button gets a screen to open. The screen reuses the Home screen's header, adds a gallery of the person's competencies, and loads its data when it opens, with a spinner while it waits and a clear message when there's nothing to show. As before, each generic task has its own short guide, and this part gives the exact values for this app.

What You'll Build​

The My Plan screen's rows, with the competency, its level and a status badge on each. This is how they looked while the screen was being built, showing every competency in the list:

The My Plan screen on the Studio canvas: a teal header with a Back button and the title My plan, and a list of light-blue rows such as Customer skill, Level: Intermediate and Leadership, Level: Beginner, each with a grey Not started badge

In the finished screen, each person sees only the competencies for their own job title:

  • A new screen, My Plan Screen, opened from My plan and left with Back.
  • A gallery of the competencies for the person's job title, sorted by name.
  • A status badge on each row, from the person's response to that competency.
  • Delegable SharePoint queries, so SharePoint does the filtering however long the lists get.
  • A loading spinner and an empty state that says why there's no plan yet.

Prerequisites​

Step 1: Add the Response Statuses to App.Formulas​

A response's status is stored as Pending, Approved or Declined (Part 2). Several screens will compare against these words, so they get names in App.Formulas, below Part 4's six formulas (How to Use Named Formulas in Power Apps):

App → Formulas (added)
StatusPending = "Pending";
StatusApproved = "Approved";
StatusDeclined = "Declined";

A typo in a named formula is caught once, in one place. A typo in a quoted word inside a formula would simply never match.

The expanded formula bar of App.Formulas ending with IsManager = !IsEmpty(MyDirectReports);, StatusPending = "Pending";, StatusApproved = "Approved"; and StatusDeclined = "Declined";

Step 2: Add the My Plan Screen​

Follow How to Add a New Screen in Power Apps: New screen → Blank. Rename the new screen My Plan Screen. Controls on it end in MPS, as the Home screen's end in HS.

Tree view with App, Home Screen and a new Screen1 at the bottom, next to the empty new screen on the canvas

Step 3: Add the Screen Container and Copy the Header​

The screen follows the Home screen's layout. This is the structure Steps 3 to 10 build:

My Plan Screen layout
My Plan Screen
└─ conMainMPS vertical: fills the screen
├─ conHeaderMPS horizontal: the header
│ ├─ btnBackMPS back to the previous screen
│ └─ lblTitleMPS "My plan", the screen's main heading
└─ conBodyMPS vertical: everything below the header
├─ lblIntroMPS what the list shows
├─ spnLoadingMPS while loading
├─ lblEmptyMPS when there's no plan
└─ galMyPlanMPS one row per competency
├─ lblCompetencyMPS
├─ lblLevelMPS
└─ bdgStatusMPS

Insert a Vertical container on the screen, rename it conMainMPS, and give it the same settings as conMainHS in Part 4 (How to Add a Vertical Container in Power Apps):

PropertyValue
X, Y0, 0
Width, HeightParent.Width, Parent.Height
Align (horizontal)Stretch
Padding0
Drop shadowNone
Border radius0

Then copy the header from the Home screen (How to Copy and Paste Controls Between Screens): Copy on conHeaderHS, Paste on conMainMPS. Rename the copies:

CopyRename toChange
conHeaderHS_1conHeaderMPSGap 16, for space between the button and the title
lblTitleHS_1lblTitleMPSText "My plan"

Everything else comes along: the theme color, the height of 64, the padding, and the title's heading Role. Each screen has its own main heading, so screen readers announce My plan here.

Tree view with conHeaderHS_1 and lblTitleHS_1 inside conMainMPS on My Plan Screen, and the new screen with the teal header copied from the Home screen

Step 4: Add the Back Button​

Select conHeaderMPS and insert Input → Button (How to Add a Button in Power Apps). Rename it btnBackMPS. It lands after the title, so open ... → Reorder → Move to start (How to Reorder Controls in a Container).

PropertyValue
Text"Back"
Icon"ArrowLeft"
Width, Height96, 40
OnSelectBack()
AccessibleLabel"Back to the Home screen"

Back returns to whichever screen opened this one (How to Go Back with Back). The accessible label starts with the visible word, so voice-control users can say Back. On the teal header, the default Primary button blends in as a white arrow and text:

The formula bar with the OnSelect property of btnBackMPS set to Back(), and the My Plan screen's teal header with the Back button and the title My plan

Step 5: Connect the My Plan Button​

On the Home screen, set the OnSelect that Part 4 left as false (How to Go to Another Screen with Navigate):

btnMyPlanHS → OnSelect
Navigate('My Plan Screen')

The screen name has spaces, so it's in single quotes.

The formula bar with the OnSelect property of btnMyPlanHS set to Navigate('My Plan Screen'), and the Home screen on the canvas

Select conMainMPS and insert a Vertical container, conBodyMPS, with the same settings as the Home screen's body:

PropertyValue
Flexible heightOn (the default)
Align (horizontal)Stretch
Gap16
Padding24 on all four sides
Drop shadowNone
Border radius0

With conBodyMPS selected, insert Layout → Vertical gallery and choose RoleCompetencies in the Select a data source flyout. Switch its Layout to Blank and rename it galMyPlanMPS (How to Add a Vertical Gallery in Power Apps):

PropertyValue
ItemsRoleCompetencies for now; Step 10 replaces it
Flexible heightOn, fill portions 1
Align in containerSet by container
TemplateSize120
TemplatePadding8
TemplateFillApp.Theme.Colors.Lighter80
AccessibleLabel"Competencies for your job title"
Selectablefalse
TabIndex0

For now, Items shows the whole list, so there are rows to design with. Selectable is false because rows don't do anything yet (Part 6 makes them open the Response screen), and TabIndex 0 lets keyboard users reach the list; without it, the App checker reports Missing tab stop.

The Select a data source flyout next to the new gallery, listing Office365Users, CompetencyResponses and RoleCompetencies under In your app, with RoleCompetencies outlined

Step 7: Show the Competency and Level in Each Row​

Select the gallery and its Edit gallery pencil, so new controls go into the template. With the gallery itself selected, they land next to it instead (Fixed: New Control Lands Outside the Gallery). Insert Display → Text twice (How to Add a Text Control in Power Apps):

PropertylblCompetencyMPSlblLevelMPS
TextThisItem.Competency"Level: " & ThisItem.Level.Value
X, Y16, 1216, 44
WidthParent.TemplateWidth - 32Parent.TemplateWidth - 32
Height3232
Size, FontWeight16, FontWeight.Semiboldthe defaults
AccessibleLabelSelf.TextSelf.Text

ThisItem is the row's competency (How to Use ThisItem), and Parent.TemplateWidth is the width of one row, so the text fits on any screen (How to Use Self and Parent).

Level is a Choice column, so its text is in .Value. Without it, Studio reports Expected Text value (How to Show a SharePoint Choice Column's Value). Each row now shows its competency and level:

The formula bar with "Level: " & ThisItem.Level.Value and Data type text, and gallery rows reading Customer skill, Level: Intermediate, Leadership, Level: Beginner, Coaching, Level: Intermediate and more

Step 8: Load the Plan When the Screen Opens​

The screen needs two things: the competencies for the person's job title, and the person's responses. It loads them in its OnVisible, which runs every time the screen is shown (How to Run a Formula When a Screen Opens). Select My Plan Screen and choose OnVisible. Build it in three pieces.

Mark the start of loading​

My Plan Screen → OnVisible (start)
UpdateContext({locIsLoading: true});

locIsLoading is a context variable: a value that belongs to this screen (How to Use Context Variables). Step 10's spinner shows while it's true.

Load both lists at once​

My Plan Screen → OnVisible (the loading)
Concurrent(
UpdateContext(
{
locMyPlan: Sort(
Filter(
RoleCompetencies,
JobTitle = MyJobTitle && IsActive
),
Competency
)
}
),
UpdateContext(
{
locMyResponses: Filter(
CompetencyResponses,
EmployeeId = MyProfile.id
)
}
)
);
  • locMyPlan holds the competencies for the person's job title that haven't been removed, sorted by name (Filter, Sort, And).
  • locMyResponses holds the person's own responses, matched by their Microsoft 365 ID.
  • Concurrent runs the two loads at the same time, because neither needs the other (How to Load Data Faster with Concurrent).

Mark the end​

My Plan Screen → OnVisible (end)
UpdateContext({locIsLoading: false})

It comes after Concurrent, so it runs only when both loads have finished. Here's the whole property, laid out with Format text:

A code window titled My Plan Screen → OnVisible: it sets locIsLoading to true, then Concurrent runs two UpdateContext calls, one loading the active RoleCompetencies for MyJobTitle sorted by competency into locMyPlan and one loading the CompetencyResponses for MyProfile.id into locMyResponses, and finally sets locIsLoading to false

My Plan Screen → OnVisible
UpdateContext({locIsLoading: true});
Concurrent(
UpdateContext(
{
locMyPlan: Sort(
Filter(
RoleCompetencies,
JobTitle = MyJobTitle && IsActive
),
Competency
)
}
),
UpdateContext(
{
locMyResponses: Filter(
CompetencyResponses,
EmployeeId = MyProfile.id
)
}
)
);
UpdateContext({locIsLoading: false})

The expanded formula bar with the OnVisible property of My Plan Screen, starting with UpdateContext locIsLoading true and Concurrent with UpdateContext for locMyPlan

note

Part 8 moves this formula into a Refresh button in the header, with Refresh on both lists first, so the screen also shows decisions a manager makes while the app is open.

Why SharePoint does the filtering​

Part 2 promised to explain delegation, and this is where it matters. Power Apps delegates a query when SharePoint can run all of it; SharePoint then sends back only the matching items. When any part can't run on SharePoint, Power Apps downloads only the first 500 items (the app's Data row limit from Part 3) and filters those, so items beyond that are silently missed (What Is Delegation in Power Apps?).

Both queries here use only what SharePoint can run (SharePoint delegation):

Query partColumn typeDelegable on SharePoint
JobTitle = MyJobTitleTextYes: = on text
IsActiveYes/NoYes: Studio shows no warning for it
&&Yes: And joins delegable parts
Sort(..., Competency)TextYes: sorting by text
EmployeeId = MyProfile.idTextYes: = on text

MyJobTitle and MyProfile.id are the same for every item, so they're sent to SharePoint as fixed values. Each query also starts with the column Part 2 indexed, JobTitle or EmployeeId, which keeps SharePoint fast as the lists grow. Studio shows no delegation warning on either. A filter that SharePoint can't run, such as !IsActive, gets Delegation warning. The "Filter" part of this formula might not work correctly on large data sets.

Why variables, not collections​

The first version of this screen used ClearCollect to load two collections (How to Create a Collection with ClearCollect). The App checker warned Collection is initialized but never updated: collections are for data the app changes, and this screen only reads. Context variables hold the same rows without that overhead.

Step 9: Add the Status Badge​

Each row shows the status of the person's response to that competency. The status is worked out once per row as a new column of the gallery's Items, so the badge's text, its color and the row's accessible label all use the same value (How to Add a Column with AddColumns). While you're still designing, keep RoleCompetencies inside it so the rows stay visible:

galMyPlanMPS → Items (while designing)
AddColumns(
RoleCompetencies As planItem,
StatusText,
Switch(
LookUp(
locMyResponses,
RoleCompetencyId = planItem.ID && Level = planItem.Level.Value
).Status.Value,
StatusPending, "Waiting for review",
StatusApproved, "Approved",
StatusDeclined, "Declined",
"Not started"
)
)

Read it from the inside out:

  1. LookUp finds the person's response to this competency, at this level (How to Find a Record with LookUp). A response belongs to a competency and a level, so if a manager changes the level, the old response doesn't count for the new one.
  2. .Status.Value is the response's status; Status is a Choice column too.
  3. Switch turns it into the words people see, and gives Not started when there's no response (How to Use the Switch Function).
  4. As planItem names the competency, so inside the LookUp planItem.Level is the competency's level and Level is the response's (How to Use the As Operator).

With the template selected, insert Display → Badge and rename it bdgStatusMPS (How to Add a Badge Control):

PropertyValue
X, Y20, 80
Width, Height144, 28
ContentThisItem.StatusText
AccessibleLabel"Status: " & ThisItem.StatusText

And its color, by meaning:

bdgStatusMPS → Intent
Switch(
ThisItem.StatusText,
"Waiting for review", BadgeIntent.Caution,
"Approved", BadgeIntent.Positive,
"Declined", BadgeIntent.Critical,
BadgeIntent.Informative
)

Base Intent on ThisItem.StatusText, not on Self.Content: the badge's own text can't be read back (Fixed: "Name Isn't Valid. 'Content' Isn't Recognized"). Finally, give the gallery a summary of each row for screen readers:

galMyPlanMPS → ItemAccessibleLabel
ThisItem.Competency & ", level " & ThisItem.Level.Value & ", " & ThisItem.StatusText

Nobody has responded yet (responses arrive in Part 6), so every row says Not started, in grey:

The expanded formula bar with AddColumns(RoleCompetencies As planItem, StatusText, Switch(LookUp(locMyResponses, ...))), Data type Table, and gallery rows each with a Not started badge under the competency and level

Step 10: Add the Intro, the Spinner and the Empty Message​

Select galMyPlanMPS and insert three controls; with the gallery selected they go into conBodyMPS, after it:

ControlInsertName
IntroDisplay → TextlblIntroMPS
SpinnerDisplay → SpinnerspnLoadingMPS
Empty messageDisplay → TextlblEmptyMPS

Then move the gallery below them: ... on galMyPlanMPS → Reorder → Move to end.

The intro says what the list is:

PropertylblIntroMPS
Text"The competencies for your job title, " & MyJobTitle & ", and where each one stands."
Auto heightOn
Visible!locIsLoading && !IsEmpty(locMyPlan)
AccessibleLabelSelf.Text

The spinner shows while the screen loads (How to Add a Loading Spinner):

PropertyspnLoadingMPS
Label"Loading your plan"
VisiblelocIsLoading

The message explains an empty plan, in the same way the Home screen explains a missing job title (How to Show a Message When a Gallery Is Empty):

lblEmptyMPS → Text
If(
IsBlank(MyJobTitle),
"Your Microsoft 365 profile has no job title, so there's no plan to show yet. Ask your IT admin to add one.",
"There's no plan for " & MyJobTitle & " yet. Competencies appear here once a manager adds them."
)
PropertylblEmptyMPS
Auto heightOn
Visible!locIsLoading && IsEmpty(locMyPlan)
AccessibleLabelSelf.Text

Now point the gallery at the person's plan instead of the whole list, and hide it while loading or empty:

galMyPlanMPS → Items
AddColumns(
locMyPlan As planItem,
StatusText,
Switch(
LookUp(
locMyResponses,
RoleCompetencyId = planItem.ID && Level = planItem.Level.Value
).Status.Value,
StatusPending, "Waiting for review",
StatusApproved, "Approved",
StatusDeclined, "Declined",
"Not started"
)
)
galMyPlanMPS → Visible
!locIsLoading && !IsEmpty(locMyPlan)

In Studio, locMyPlan is empty until OnVisible has run, so the canvas shows the message:

The finished My Plan screen on the Studio canvas: the teal header with Back and My plan, and the message There's no plan for Managing Director yet. Competencies appear here once a manager adds them

Step 11: Test the Screen​

Select Home Screen, preview with F5 (How to Preview a Canvas App), and select My plan. The spinner shows for a moment:

The My Plan screen in preview with a spinner and Loading your plan below the header

Then, for the admin, the empty state. The Managing Director has no competencies, on purpose (Part 2), so this is the right result:

The My Plan screen in preview with the message There's no plan for Managing Director yet. Competencies appear here once a manager adds them

Select Back, and the Home screen returns. At a phone size (Phones → Apple iPhone 12, 13, 14, How to Test on Phone and Tablet Sizes) the message wraps onto three lines:

The My Plan screen in an iPhone frame with the header and the empty-state message on three lines

People whose job title has competencies see them listed with a badge each. Part 7 shares the app and opens it as Olivia Bennett, the Head of Sales.

Step 12: Check the App Checker​

Open the App checker and select Recheck all.

  • Data source: the warning from Part 4, Unused data sources, is gone, because OnVisible now reads both lists.
  • Performance: no count, because the screen uses variables rather than collections.
  • Accessibility (2): Focus isn't showing and Missing tab stop, both on lblTitleMPS.

The App checker with Recheck all and the categories Formulas, Runtime, Accessibility (2), Performance and Data source, with no count next to Performance and Data source

The two remaining items stay. lblTitleMPS is the screen's heading and does nothing when selected. Microsoft's guidance is to keep TabIndex -1 on text that isn't interactive, and it notes that screen readers announce a label with TabIndex 0 as a button. Making the heading a tab stop would do more harm than the warning, and the checker's documentation says to ignore suggestions like that. The Home screen's heading has the same settings and isn't listed. Both fixes posts explain the reasoning.

Step 13: Save the App​

Select Save. Don't publish yet: Part 7 publishes the app and shares it.

The finished screen's controls in Tree view, matching the layout from Step 3:

The tree view of My Plan Screen: conMainMPS with conHeaderMPS (btnBackMPS and lblTitleMPS) and conBodyMPS (lblIntroMPS, spnLoadingMPS, lblEmptyMPS and galMyPlanMPS with bdgStatusMPS, lblLevelMPS and lblCompetencyMPS)

The complete App.Formulas after this part:

App → Formulas
AppTitle = "Career Development Plan";
MyProfile = Office365Users.MyProfileV2({'$select': "id,displayName,givenName,mail,userPrincipalName,jobTitle"});
MyJobTitle = Trim(MyProfile.jobTitle);
MyManager = IfError(
Office365Users.ManagerV2(
MyProfile.id,
{'$select': "id,displayName,mail,jobTitle"}
),
Blank()
);
MyDirectReports = Sort(
Filter(
Office365Users.DirectReportsV2(
MyProfile.id,
{'$select': "id,displayName,mail,userPrincipalName,jobTitle,accountEnabled"}
).value,
accountEnabled
),
displayName
);
IsManager = !IsEmpty(MyDirectReports);
StatusPending = "Pending";
StatusApproved = "Approved";
StatusDeclined = "Declined";

Common Mistakes​

MistakeWhat happensDo this instead
Inserting the row's controls with the gallery selectedThey land next to the gallery, not in each rowSelect the template with the Edit gallery pencil
ThisItem.Level without .ValueExpected Text valueThisItem.Level.Value
!IsActive in the filterA delegation warning, and missed items on big listsIsActive or IsActive = false
Leaving Items as RoleCompetenciesEveryone sees every job title's competenciesUse the Step 10 formula with locMyPlan
Writing AddColumns before OnVisiblelocMyResponses isn't recognized yetWrite OnVisible (Step 8) first
Removing the lists because of Part 4's warningNothing to loadKeep them; the warning clears now

Troubleshooting​

SymptomLikely causeFix
Expected Text value on the levelA Choice column used as textFixed: "Expected Text Value"
Name isn't valid. 'Content' isn't recognizedSelf.Content on the badgeFixed: "Name Isn't Valid"
A Text control appears beside the galleryThe gallery, not its template, was selectedFixed: New Control Lands Outside the Gallery
A yellow triangle on the galleryPart of the filter can't run on SharePointFixed: Delegation Warning
Collection is initialized but never updatedClearCollect used for read-only dataFixed: "Collection Is Initialized but Never Updated"
Missing tab stop on the galleryTabIndex still -1Fixed: "Missing Tab Stop"
The screen is empty in StudioOnVisible runs only when the screen is shownPreview from the Home screen and select My plan
The spinner never goes awaylocIsLoading isn't set back to falseCheck the end of OnVisible

FAQ​

Why is my plan empty?​

In the demo organization, the admin is the Managing Director, and Part 2 gave that job title no competencies on purpose. Anyone whose job title has competencies sees them. If you follow along with your own job title, add a few competencies for it in RoleCompetencies, and the list fills in.

Why does every competency say "Not started"?​

Nobody has responded yet. Part 6 builds the screen where people write and send their responses, and the badges change to Waiting for review.

Why load the data in OnVisible and not in App.Formulas?​

The screen shows a spinner while loading, and its empty message only once it knows there's nothing to show. OnVisible gives a clear start and end for that. Fixed: "Collection Is Initialized but Never Updated" compares the two options.

Will it work with thousands of competencies?​

Yes. Both queries are delegated, so SharePoint filters the lists and returns only one person's competencies and responses, starting from indexed columns.

Why is the heading still listed in the App checker?​

Because the fix the checker suggests would make screen readers announce the heading as a button. Step 12 and the two fixes posts explain why it's left as it is.

Conclusion​

The app now has its second screen. My plan on the Home screen opens My Plan Screen, which:

  • loads the competencies for the person's job title and their responses when it opens, with delegable SharePoint queries;
  • lists the competencies with their level and a status badge;
  • shows a spinner while loading, and explains an empty plan;
  • returns with Back, fits a phone, and has accessible labels throughout.

← Previous: Part 4: Build the Home Screen

Next → Part 6: Build the Response Screen. You'll make each competency open a screen where the person writes their response and sends it to their manager.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.