Download CVCV

Power Apps combo box not listing SharePoint group members

Power AppsPower AutomateSharePointPower Fx


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. Items is a collection that already holds the rows, so there is no enumeration left to fail at open time.
  • Typing filters them. SearchFields has 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 Update rebuilds 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.