Filter Rows: The Lookup GUID Is _parentcustomerid_value
In this series: Power Automate gotchas
The contacts-for-this-account flow listed every contact in the environment. Filter rows had been parentcustomerid eq 'Contoso Ltd' until List rows failed, so the maker cleared the box and moved the check into Filter array. Apply to each ran for minutes. The account name was never a column Dataverse would compare in Filter rows. Filter rows is OData against Dataverse attributes. A lookup's GUID lives in _parentcustomerid_value, not in parentcustomerid, and not in the name you see on the form.
That is the whole property. Filter array is a different action, on a payload you already paid to download. Putting Filter array syntax in Filter rows does not "filter harder." It sends a string Dataverse cannot evaluate as $filter.
Two boxes, two languages
Filter rows on List rows is $filter. Dataverse evaluates it. You get back the rows that match, or an empty value array if none do — the same successful empty as a business-key miss. A broken OData string is a failed List rows. Those are not the same outcome.
Filter array is a Data Operations action. It walks body/value (or whatever array you hand it) in the flow. The language is Workflow Definition Language: @equals, item(), contains on the current row. Dataverse never sees it. You already pulled the page.
List rows Filter rows
parentcustomerid eq 'Contoso Ltd'
→ Dataverse does not treat parentcustomerid as a name column.
You wanted the account's GUID on _parentcustomerid_value.
List rows Filter rows
@equals(item()?['parentcustomerid'], variables('AccountId'))
→ That is Filter array syntax. There is no item() in $filter.
The string is not valid OData.
Filter array From: outputs('List_rows')?['body/value']
@equals(item()?['_parentcustomerid_value'], variables('AccountId'))
→ Valid. Late. You already listed contacts you did not need.
The Apply to each post already says Filter array is in-memory reduction after the fetch. This post is the lookup column, the OData name, and the query that has to run in Dataverse if you care which rows come back.
The attribute you actually filter
On the contact form, Parent Customer is parentcustomerid. That is the lookup. In the Web API payload, and in Filter rows, the foreign key is _parentcustomerid_value: a GUID. The display name you clicked in the lookup control is not stored in parentcustomerid. It is resolved from the related account or contact.
List rows
Table name Contacts
Filter rows _parentcustomerid_value eq '@{variables('AccountId')}'
Select columns contactid,fullname,_parentcustomerid_value
AccountId is the account's primary key from this run: the trigger body's accountid, an Add a new row output, a Get a row you already did. Interpolate it into the OData string. The quotes around the expression are the usual maker pattern and Dataverse accepts that GUID.
Do not put the account name in that predicate. Names are not unique, and this column is not a name. If you only have a name, List rows on Accounts with name eq 'Contoso Ltd' (and a row count of 2, so you can see duplicates), take the accountid, then filter contacts on _parentcustomerid_value. Two queries, one GUID. That is find. Comparing a name to a lookup field is not.
The same suffix applies to every lookup:
| Form label | Logical name | Filter rows |
|---|---|---|
| Parent Customer | parentcustomerid | _parentcustomerid_value eq {guid} |
| Primary Contact | primarycontactid | _primarycontactid_value eq {guid} |
| Owner | ownerid | _ownerid_value eq {guid} |
| Owning Business Unit | owningbusinessunit | _owningbusinessunit_value eq {guid} |
The pattern is _ + lookup logical name + _value. Copy the logical name from the column in the maker portal. Do not guess from the display label. Parent Customer is not an OData identifier.
Select columns on List rows is a payload trim — logical names, comma-separated — the same idea as Select columns on the trigger, except here it does not decide whether the flow starts. Include _parentcustomerid_value if a later action needs the GUID. You do not need the lookup's display name in Select columns to filter on the GUID.
Integers, strings, and @equals
OData is typed. statuscode eq 1 is an integer. statuscode eq '1' is a string. On account and contact, Active is statecode eq 0 and statuscode eq 1. The form label "Active" is formatted text. It is not the filter. Quote strings (accountnumber eq 'US-001'). Do not quote the integer choice.
@equals(item()?['statuscode'], 1) belongs in Filter array, or in a Condition. In Filter rows it is characters Dataverse will not parse as $filter. The Get a row post called this out for accountnumber. Lookups are the case that still ships with the navigation name in the box, because that is the name on the form.
contains(fullname, 'smith') is Dataverse OData. contains(item()?['fullname'], 'smith') is Filter array. Same English word, two grammars. item() only exists when there is a current row in the flow.
If the interpolated value can contain a single quote (O'Brien), escape it before you concatenate. A quote inside an OData string terminates the literal. The action then fails. That is a failed List rows, not an empty list.
Expand is how you read the related row
Expand Query on List rows is $expand. You name the navigation property, not _…_value. For a simple lookup that is often the logical name (primarycontactid). For a Customer lookup (account or contact) Dataverse exposes two navigations: parentcustomerid_account and parentcustomerid_contact. Expand the side you need. The GUID filter still uses _parentcustomerid_value either way.
List rows
Table name Contacts
Filter rows _parentcustomerid_value eq '@{variables('AccountId')}'
Expand Query parentcustomerid_account
Select columns contactid,fullname,_parentcustomerid_value
Expand is for fields on the parent in the same call. It is not a substitute for Filter rows. Expanding parentcustomerid_account and then Filter array on the account name still downloaded every contact on the page. Filter on the GUID first. Expand second, if you need the parent's columns.
You can filter on a related attribute in OData with the navigation: parentcustomerid_account/name eq 'Contoso Ltd'. Use that when you truly have only a name and cannot look the account up first. It is still a name, so it is still not unique. Prefer the GUID.
Filter array is the leftover test
Use Filter array when the predicate cannot be OData: a value you computed in the flow, a match against another array you already hold, a formatted annotation that is not a Dataverse column. The connector's payload includes a formatted display name next to _parentcustomerid_value. Filter array can read that. Filter rows cannot. If the rule is "this account's GUID," you do not need the name, and you should not download the table to find it.
List rows
Table name Contacts
Filter rows _parentcustomerid_value eq '@{variables('AccountId')}'
Row count 500
Filter array only if a second, non-OData rule remains
From outputs('List_rows')?['body/value']
Row count caps the page. It does not replace Filter rows. Five hundred unfiltered contacts and a Filter array on the GUID is still five hundred rows across the wire, then a reduction. Put the GUID in Filter rows. Keep Row count as a safety cap, and turn on pagination only when the filtered set is honestly larger than one page.
empty(body('Filter_array')) after a Filter array is a valid branch. It is also a smell when List rows had no Filter rows. You listed, then discarded. The discard should have been a where clause in Dataverse.
SharePoint is the same shape with different names. Get items Filter Query is OData. A lookup column is Account/Id eq 12 or Account/Title eq 'Contoso Ltd', not Account eq 'Contoso Ltd'. Filter array on item()?['Account']?['Id'] is again after the download. Get item by id is still not a lookup-by-title.
What to type in Filter rows
_logicalname_value eqplus a GUID this run already owns.statecode eq 0,statuscode eq 1,accountnumber eq 'US-001'— attributes, typed literals.- Combined with
and/orin OData, not nested Condition actions that run per row. - Not the lookup's form label, not
parentcustomerid eq 'Contoso Ltd', not@equals(...). - Not a display name you hope Dataverse will resolve. Resolve the name to a GUID with List rows on the parent table, then filter children on
_…_value.
Filter rows is the query. Filter array is a second pass over rows you already have. For a lookup, the column that query understands is _parentcustomerid_value. Put the GUID there, and List rows returns the contacts that belong to that account — or a successful empty array, which is a Condition, not a loop.
Keep reading
Get a Row by ID: Missing Fails the Run, Empty List Rows Does Not
Get a row by ID fails the run when the GUID is missing. List rows with zero matches succeeds. Look up the business key and branch on empty.
When an HTTP Request Is Received: The Callback URL Is a Secret
The HTTP trigger URL carries a SAS signature. Anyone with it can start the flow. Restrict who can call, always Respond, and never treat that URL as public.
Power Automate Do Until: The Loop That Succeeds After Giving Up
A Do until that hits count or timeout still succeeds. Put Get inside the loop, add a Delay, and test the condition after the loop exits.
Power Automate Trigger Conditions: The Run That Never Appears
A trigger condition that is false does not start a run and does not leave run history. How the expression differs from a Condition action, and how to see the skipped checks.
Power Platform September 2026 Update: The App-Builder Skill, Agent Plugins, and the Refreshed Model-Driven UI
What matters in Microsoft's September 2026 Power Platform feature update: the model app-builder skill goes GA, the canvas authoring agent plugin goes GA, generative pages embed in forms and reach connectors, model-driven header refresh is GA with display density in preview, and Power Automate gets server-side search.
Dataverse Triggers in Power Automate: Select Columns or the Flow Wakes Up for Nothing
How Select columns and Filter rows on a Dataverse trigger cut wasted runs, and the row-version mistakes that make a flow miss the change it was built for.
Newsletter
New posts, straight to your inbox
One email when a new post goes up. Double opt-in, unsubscribe anytime. Not a daily digest.
Comments
- No comments yet. Be the first.