Power Apps combo box not listing SharePoint group members
You have a SharePoint list with an Approver Person column, and the rule is that only members of
the site’s Project Approvers group should be selectable.
SharePoint has a setting for exactly this. On the column, under Choose from, you point it at a SharePoint group instead of All Users. In SharePoint’s own form it behaves.
Then you open the customised Power Apps form, click the dropdown, and nothing appears.
Type a name and the person is found, correctly limited to the group. So the rule is being enforced. What you have lost is the list. Nobody can browse the approvers, they can only confirm a name they already knew. Flip the column back to All users and the dropdown fills up again, which is what makes it feel like a bug rather than a design.
That is the problem this solves. Not search, which already works. The list.
Why the obvious approaches fall short
Choices([@'Your List'].Approver), the default Items for a Person data card, resolves against
every user the list can see. The dropdown becomes your entire directory, and you get the first 500
principals with no warning that the rest are missing.
The column’s “Choose from: SharePoint Group” setting enforces the constraint but does not populate the dropdown. The setting is not ignored, since search obeys it. The control just will not enumerate the group for you.
This is reported and unresolved in both of Microsoft’s own venues. On Microsoft Q&A someone describes “the ComboBox is not listing the members of the SharePoint” and adds that “if I change this to Choose from: All users, it works but I don’t want it to list everyone in the company”. On the Power Platform community forum, another thread reports the dropdown failing to display options on open, showing users “only when they manually type and search for a name”. Neither gets a mechanism, and the accepted answer on the Q&A sidesteps the field by hiding it and auto-populating the current user.
Why enumeration fails while search works, I do not know. Choices() is not delegable and carries the
500 to 2,000 row ceiling, so an undelegable enumeration is a plausible culprit, but Microsoft does not
document the combination and I have not proved it.
It is not a missing text column, at least: Choices() on a Person column returns Claims,
DisplayName, Email, Department, JobTitle and Picture. And it is not group-backed pickers
being broken in general.
Office365Groups.ListGroupMembers("<guid>").value proves that last point, and it is the right
answer if your group is a Microsoft 365 or Entra group. You get a populated, searchable dropdown
because the connector returns an ordinary table.
A SharePoint site group is a different object. It is the kind you create inside a site under Site
permissions > Advanced permissions settings, the kind with a MembershipGroupId like 14. It is not
an Entra object, it exists only in SharePoint’s content database, and no Power Apps connector can
enumerate it.
So the gap is narrow. If your approvers live in an Entra group, use Office365Groups and stop reading.
If they live in a SharePoint site group, nothing will hand you a table of members, so you fetch them
yourself.
What changes where
Two tools, six changes. Nothing else in the form is touched.
| Where | Change |
|---|---|
| Power Automate | One instant flow returning the group’s members as a JSON string |
Form step 1: App.OnStart |
Call the flow, parse the result into colApprovers |
| Form step 2: the ComboBox, inside the card | 2.1 Items to colApprovers · 2.2 DisplayFields and SearchFields to ["DisplayName"] · 2.3 DefaultSelectedItems to re-select the saved person · 2.4 SelectMultiple to match the column |
| Form step 3: the data card itself | Update, to rebuild a valid Person value on save |
The collection needs exactly three fields: Claims, DisplayName and Email. Claims is the unique
key you match, display and re-select on. Everything else a Person field wants gets rebuilt at save
time.
What that buys, and it follows from the mechanics rather than from luck:
- The dropdown lists the members.
Itemsis a collection that already holds the rows, so there is no enumeration left to fail at open time. - Typing filters them.
SearchFieldshas a plain text column to match on and no delegation in the way. - The constraint holds, but note that it now holds because the collection contains only group
members. It is enforced by what you put in
Items, not by SharePoint’s picker. Leave the column’s Choose from setting in place if you want SharePoint enforcing it too. - The saved value is a real Person value, because
Updaterebuilds one. - The list follows the group. Add or remove someone in SharePoint and the picker changes with no edit to the app.
And what it does not buy. The membership is a snapshot taken when the flow last ran, not a live view. Nested groups are not expanded. Nobody can pick someone outside the group, which is the point, but it also means this is the wrong control if that ever needs to be an exception.
In Power Automate
An instant flow, three actions. Everything you need to rebuild it is in the tables below: the URI, the header, the three mappings and the output expression.
The flow and the form formulas are the ones running in production, unchanged apart from renaming things. Where there is a hardening change worth making later, I have said so at that point rather than folding it into the code, because none of them are needed to make this work.
Trigger: PowerApps (V2)
Add two required Text inputs:
| Input | Title | Example |
|---|---|---|
| Text | siteUrl |
https://contoso.sharepoint.com/sites/Projects |
| Text | groupName |
Project Approvers |
Parameterising both is what makes the flow reusable. One flow serves every group in every site, instead of one flow per picker.
Action 1: Send an HTTP request to SharePoint
| Field | Value |
|---|---|
| Site Address | siteUrl (from the trigger) |
| Method | GET |
| Uri | _api/web/sitegroups/getbyname('@{triggerBody()?['text_1']}')/users?$select=Title,Email,LoginName |
| Headers | Accept : application/json;odata=nometadata |
triggerBody()?['text_1'] is the second text input, the one titled groupName. The PowerApps (V2)
trigger names its inputs text, text_1, text_2 internally regardless of the titles you give them,
so text is siteUrl and text_1 is groupName. If you build the Uri with the dynamic content
picker you will see groupName and get the same expression underneath.
odata=nometadata flattens the response to a plain value array. Without it you get d/results
wrappers and a lot of __metadata noise to navigate around.
If your group is large, check what actually comes back before trusting it. Adding &$top=500 to the
Uri is a cheap precaution, and past a few hundred members a dropdown is probably the wrong control
anyway.
Action 2: Select
From: body('Send_an_HTTP_request_to_SharePoint')?['value']
Map (switch to text mode with the icon on the right). Three keys:
DisplayName
@{item()?['Title']}
Email
@{toLower(item()?['Email'])}
Claims
@{if(startsWith(item()?['LoginName'], 'i:0#.f|membership|'), item()?['LoginName'], concat('i:0#.f|membership|', toLower(coalesce(item()?['Email'], item()?['LoginName']))))}
The claims value is in a code block rather than a table because the pipe characters in
i:0#.f|membership| would have to be escaped inside a table, and an escaped pipe copied into Power
Automate is a broken claim.
A Select rather than an Apply to each with an append: one action, one pass, and the output is already the shape Power Apps wants.
The Claims expression is defensive on purpose. In SharePoint Online a normal user’s LoginName is
already the full claim, so the first branch handles essentially every real row. The rest covers hybrid
and on-premises identities.
If your group contains nested groups
/users on a site group returns principals, not only people. Nest an Entra security group inside the
site group and you get a row for the group itself, with a blank email and a LoginName shaped like
c:0t.c|tenant|<guid>. It shows up in the dropdown as a selectable entry that is not a person.
For a group of individual users, the common case, this never comes up. If you do have nested groups,
add PrincipalType to the $select and put a Filter array action before the Select, keeping only
PrincipalType equal to 1. The values are 1 for User, 2 for distribution list, 4 for security
group and 8 for SharePoint group. That drops nested groups rather than expanding them, so their
members appear as nothing rather than as an error.
Those blank emails are also worth knowing about because of the coalesce() in the claims expression
above. coalesce() returns the first non-null value, and SharePoint returns a missing email as an empty
string, which is not null. So coalesce("", loginName) returns "", and the claim becomes exactly
i:0#.f|membership| with nothing after it. That value is truthy and non-blank, so it lands in the
collection looking perfectly healthy and only surfaces later as a Person field that will not save.
For a group of individual users this never fires, because everyone has a mailbox and the first branch
of the if handles them. If you add nested groups, swap coalesce(...) for an explicit
if(empty(item()?['Email']), item()?['LoginName'], toLower(item()?['Email'])), or filter the
non-users out as above, which removes the case entirely.
Action 3: Respond to a Power App or flow
Add one Text output:
| Name | Value |
|---|---|
usersjson |
@{string(body('Select'))} |
string() is required. Respond to a Power App or flow can only return scalars, so the array is
serialised here and parsed back on the Power Apps side. That is the only reason ParseJSON appears
later.
Expected shape:
[
{
"Claims": "i:0#.f|membership|jane@contoso.com",
"DisplayName": "Jane Doe",
"Email": "jane@contoso.com"
}
]
Run the flow on its own before going near Power Apps. Almost every “the dropdown is still empty” problem turns out to be the flow, and it is much harder to diagnose from inside the app.
In the customised form
Add the flow to the app first: Power Automate > Add flow.
Everything after step 1 happens on the Approver data card, the one generated for the Person column. Unlock it before you start, or its properties stay read-only: select the card, then Advanced > Unlock to change properties.
Worth knowing that the card and the control inside it are two different things, because the changes
below are split across both. The card holds the Update property, which decides what gets written to
the column. The ComboBox sits inside the card and holds Items, SearchFields and the rest. Clicking
in the middle of the card usually selects the ComboBox, so use the tree view on the left to pick
whichever one you actually want.
Three steps. Within step 2, 2.1 and 2.2 are what actually fix the empty dropdown; 2.3 and 2.4, and
step 3, are the cost of having stopped using Choices(), since you now own the round trip back to a
valid Person value.
1. App.OnStart: build the collection
ClearCollect(
colApprovers,
SortByColumns(
ForAll(
Table(
ParseJSON(
'GetSPGroupMembers'.Run(
"https://contoso.sharepoint.com/sites/Projects",
"Project Approvers"
).usersjson
)
),
{
Claims: Text(Value.Claims),
DisplayName: Text(Value.DisplayName),
Email: Text(Value.Email)
}
),
"DisplayName"
)
)
| Element | Why it’s there |
|---|---|
.Run(...).usersjson |
Executes the flow and grabs the string output |
ParseJSON |
Turns the string into an untyped object |
Table(...) |
Turns the untyped array into a single-column table whose column is named Value |
ForAll |
Casts each value and keeps only the three fields. If a JSON key ever collides with a control or column name, add an As alias (ForAll(Table(...) As _row, ...)) and qualify the references |
Text(...) |
Untyped objects need an explicit cast, or the columns stay untyped and the ComboBox will not bind |
SortByColumns |
Alphabetical dropdown |
App.OnStart beats Screen.OnVisible here. The form app loads once and then services New, Edit and
View, so the flow fires once per session instead of on every item opened. The trade-off is staleness,
since membership changes will not appear until reload. Use OnVisible if your group changes often.
Several pickers on one form. Duplicate the block per group into its own collection, so
colApprovers and colReviewers rather than one shared table. Keep the projected shape identical and
every formula below stays copy-pasteable between them.
2. The ComboBox properties
All four live on the ComboBox inside the Approver data card, not on the card itself.
2.1 Items
colApprovers
That one line is what fixes the original problem. The dropdown now has rows because the collection has rows, with nothing to enumerate at open time.
2.2 DisplayFields and SearchFields
["DisplayName"]
Set both, not just one. They keep whatever the Person field originally put there, so after you repoint
Items the control is still trying to display and search against fields that no longer exist in the
new schema. You will see a ComboBox that renders raw i:0#.f|membership|... claims strings, or a
search box that types but never filters.
2.3 DefaultSelectedItems
This re-selects the saved person when the form opens in Edit or View mode.
If(
IsBlank(
LookUp(
colApprovers,
Claims = ThisItem.Approver.Claims
)
),
[ThisItem.Approver],
[
LookUp(
colApprovers,
Claims = ThisItem.Approver.Claims
)
]
)
It looks the saved person up in the collection by Claims. If they are still a member you get the
collection’s own record back, which is what makes the ComboBox show them as selected, since it matches
DefaultSelectedItems against Items by value across every field.
The IsBlank branch covers someone who has left the group. It falls back to the raw stored value, so
their name still shows rather than being silently erased from a historical record.
Two notes if you want to harden this later. The fallback returns a full SPListExpandedUser while
Items holds three columns, so if you ever see a schema complaint, project it down to
{ Claims: ..., DisplayName: ..., Email: ... }. And the LookUp runs twice as written, which you can
collapse with With({ _match: LookUp(...) }, ...) if the collection is large or the form has several
of these.
2.4 SelectMultiple
false for a single Person column, true for Person (multi). It changes which Update formula you
need in step 3.
3. The data card’s Update: write a valid Person value
Select the Approver data card itself this time, not the ComboBox inside it. Update is a card
property, and it is what the form writes to the column on save.
The collection’s records carry three fields, which is not a valid Person value, so Update rebuilds a
full SPListExpandedUser.
{
'@odata.type': "#Microsoft.Azure.Connectors.SharePoint.SPListExpandedUser",
Claims: ComboBoxApprover.Selected.Claims,
DisplayName: ComboBoxApprover.Selected.DisplayName,
Email: ComboBoxApprover.Selected.Email,
Department: "",
JobTitle: "",
Picture: ""
}
Department, JobTitle and Picture can be empty. SharePoint only needs Claims to resolve the user
and repopulates the rest itself.
If the column is optional, wrap it so an empty selection writes Blank() rather than an object
full of empty strings, which would otherwise read as “someone is assigned” to every downstream
IsBlank() check, view filter and flow condition:
If(
IsBlank(ComboBoxApprover.Selected),
Blank(),
{
'@odata.type': "#Microsoft.Azure.Connectors.SharePoint.SPListExpandedUser",
Claims: ComboBoxApprover.Selected.Claims,
DisplayName: ComboBoxApprover.Selected.DisplayName,
Email: ComboBoxApprover.Selected.Email,
Department: "",
JobTitle: "",
Picture: ""
}
)
For a Person (multi) column
ForAll(
ComboBoxApprovers.SelectedItems As _sel,
{
'@odata.type': "#Microsoft.Azure.Connectors.SharePoint.SPListExpandedUser",
Claims: _sel.Claims,
DisplayName: _sel.DisplayName,
Email: _sel.Email,
Department: "",
JobTitle: "",
Picture: ""
}
)
The As _sel alias matters. Unqualified, Claims is ambiguous between the SelectedItems column and
the record you are building, and the error you get is not a helpful one.
The matching DefaultSelectedItems:
ForAll(
ThisItem.Approvers,
If(
IsBlank(LookUp(colApprovers, Claims = Claims)),
{ Claims: Claims, DisplayName: DisplayName, Email: Email },
LookUp(colApprovers, Claims = Claims)
)
)
This one is untested, unlike the single-select version above. Claims is also ambiguous here between
the saved record and the collection, so if Power Apps complains, add an As alias to the ForAll and
qualify both sides.
One permissions trap before you ship
Adding a flow to an app shares it for run-only use, and each connector defaults to “Provided by run-only user”. That means the group lookup runs as whoever opened the form. Users who cannot read the site’s permission data get an empty dropdown, so the form looks broken for some people and fine for others. It always works for you, because you own the connection.
In the flow’s share settings, switch the SharePoint connector to the embedded connection, and make the owner a service account rather than a person.
Placeholder checklist
| Placeholder | Replace with |
|---|---|
GetSPGroupMembers |
The flow’s name as added to the app |
https://contoso.sharepoint.com/sites/Projects |
Your site URL |
Project Approvers |
Your group’s display name |
colApprovers |
A unique collection name per group |
ThisItem.Approver |
Your Person column. Quote it if it has spaces: ThisItem.'Project Approver' |
ComboBoxApprover |
Your ComboBox’s control name, for example DataCardValue11 |
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Dropdown still empty | getbyname() wants the group’s display name, not its MembershipGroupId. Passing the number 404s into an empty array with no error. Run the flow on its own first |
| Dropdown empty for some users only | Run-only connection is “Provided by run-only user”. Switch to the embedded connection |
| ComboBox shows the raw claims string | DisplayFields not set to ["DisplayName"] |
| Search box does nothing | SearchFields not set, or IsSearchable = false |
| Saved person not pre-selected on edit | Claims mismatch, usually email casing. Return _match from the collection rather than a rebuilt record |
Schema error on DefaultSelectedItems |
Fallback returns a full SPListExpandedUser against three-column Items. Project it down |
| Person saves but renders blank | Truncated claim, from the coalesce trap above |
| Optional field never reads as empty | Update writes an empty-string object instead of Blank(). Add the IsBlank guard |
| Malformed URI error | Apostrophe in the group name. Double it: getbyname('Managers'' Group') |
| A group appears as a selectable entry | A nested Entra group. Filter on PrincipalType eq 1 |
Three things are worth remembering rather than looking up. Claims is the unique key, so match,
display and re-select on it. Keep the collection shape identical everywhere, including the fallback
branches. And always rebuild the full user object on Update, rather than patching the raw three-field
record to the list, which is a schema error waiting to happen. Most of the table above is a consequence
of breaking one of those three.