---
title: "How to Dynamically Filter Payload CMS Relationship Fields Based on Sibling Data in Array Fields"
slug: "payload-dynamic-relationship-filtering-array-fields"
published: "2025-10-12"
updated: "2025-12-26"
validated: "2025-10-20"
categories:
  - "Payload"
tags:
  - "payload cms relationships"
  - "dynamic filtering"
  - "filteroptions"
  - "sibling data"
  - "payload array fields"
  - "relationship fields"
  - "conditional dropdowns"
  - "payload cms v3"
  - "collection filtering"
  - "nested fields payload"
llm-intent: "reference"
audience-level: "advanced"
framework-versions:
  - "payload@3"
  - "typescript@5"
  - "node@20"
status: "stable"
llm-purpose: "Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips"
llm-prereqs:
  - "Access to Payload CMS"
  - "Access to TypeScript"
  - "Access to Node.js"
llm-outputs:
  - "Completed outcome: Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips"
---

**Summary Triples**
- (Server-side filterOptions, should use, siblingData to build the relationship query when filtering options inside array rows)
- (filterOptions function, has signature, async ({ siblingData, req, currentUser }) => filterObject and runs on the server)
- (siblingData, contains, same-row sibling field values available to server filterOptions for an array row)
- (Typical client-side custom fields, do not trigger, relationship re-queries inside array rows when sibling values change (so they fail to keep dropdowns in sync))
- (Working pattern, is, define relationship filterOptions that return a filter referencing siblingData (e.g., { product: { equals: siblingData.product } }))
- (Debugging, recommendation, log siblingData inside filterOptions, verify relation field names and types, and run payload.find to confirm the filter)
- (Alternative approach, is, build a fully custom admin React field that fetches options when sibling value changes and populates a controlled dropdown)

### {GOAL}
Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips

### {PREREQS}
- Access to Payload CMS
- Access to TypeScript
- Access to Node.js

### {STEPS}
1. Map the array structure
2. Implement sibling-aware filterOptions
3. Trace the request lifecycle
4. Check implementation requirements
5. Avoid common pitfalls
6. Extend the pattern

<!-- llm:goal="Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips" -->
<!-- llm:prereq="Access to Payload CMS" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to Node.js" -->
<!-- llm:output="Completed outcome: Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips" -->

# How to Dynamically Filter Payload CMS Relationship Fields Based on Sibling Data in Array Fields
> Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips
Matija Žiberna · 2025-10-12

I was building an inventory management system with Payload CMS when I hit a frustrating wall. I had an array field with products and variants, and I needed the variant dropdown to show only variants belonging to the selected product. Sounds simple, right? The standard `filterOptions` approach wasn't working, custom field components with `useFormFields` weren't triggering re-queries, and I kept seeing all variants in the dropdown regardless of which product was selected.

After several hours of debugging and trying different approaches, I discovered the correct pattern. This guide shows you exactly how to implement dynamic relationship filtering in Payload CMS array fields using server-side filtering with `siblingData`. If you prefer building fully custom React components to fetch options on the client, I walk through that alternative in [How to Create Dynamic Cross-Collection Dropdowns in Payload CMS v3](https://www.buildwithmatija.com/blog/dynamic-cross-collection-dropdowns-payload-cms-v3).

## The Problem: Relationship Fields Showing All Options

When you have nested fields in Payload CMS arrays, filtering relationship options based on sibling field values isn't straightforward. Here's the scenario I encountered:

I had a collection with an array field containing products and their variants. Each array row had two relationship fields: one for selecting a product, and one for selecting a variant. The variant field needed to show only variants that belonged to the selected product in that same row.

The typical approaches that developers try first all fail in this context:

**Static filterOptions don't work** because they can't access the dynamic product selection. You might try something like this:

```typescript
// File: src/collections/DeliveryWindowBatches.ts
{
  name: "variant",
  type: "relationship",
  relationTo: "product-variants",
  filterOptions: {
    product: {
      equals: 2  // This is static - doesn't help
    }
  }
}
```

This filters variants, but only to a hardcoded product ID. It doesn't respond to user selections.

**Custom field components seem promising** but fail because they modify filterOptions client-side, which doesn't trigger the RelationshipField to re-fetch options from the server. I tried building a custom component using `useFormFields` to read the product value and modify the field config:

```typescript
// File: src/components/fields/FilteredVariantField.tsx (DOESN'T WORK)
'use client'

import { RelationshipField, useFormFields } from '@payloadcms/ui'

export const FilteredVariantField = (props) => {
  const { path, field } = props
  const productPath = path.replace('.variant', '.product')

  const product = useFormFields(([fields]) => {
    return fields[productPath] || null
  })

  const productId = product?.value

  const fieldWithFilter = {
    ...field,
    filterOptions: productId ? {
      product: { equals: productId }
    } : {}
  }

  return <RelationshipField {...props} field={fieldWithFilter} />
}
```

The problem here is that Payload's fields are stored flat with dot notation as keys like `fields['products.0.product']`, not nested like `fields.products[0].product`. Even after fixing that path issue, the component reads the product value correctly and creates the filter, but the RelationshipField component doesn't re-query the API when the filterOptions object changes. The dropdown still shows all variants because the filter is applied client-side after the data has already been fetched.

**Error messages you might encounter:**
- "Payload CMS filterOptions not working" - The filter object is ignored
- "Payload relationship field shows all options" - Dropdown shows everything
- "useFormFields relationship filter" - The hook returns data but doesn't trigger re-fetches

The core issue is that `filterOptions` is evaluated server-side when Payload makes the API call to fetch relationship options. Modifying the field config client-side has no effect on that server-side query.

## The Solution: Server-Side filterOptions with siblingData

The correct approach uses `filterOptions` as a function that runs server-side and has access to `siblingData`. This function is called when Payload fetches the relationship options, and it receives context about other fields in the same array row.

Here's the working implementation:

```typescript
// File: src/collections/DeliveryWindowBatches.ts
{
  name: "products",
  type: "array",
  fields: [
    {
      type: "row",
      fields: [
        {
          name: "product",
          type: "relationship",
          relationTo: "products",
          required: true,
          filterOptions: {
            inventoryAllocationMode: {
              equals: 'delivery_window',
            },
          },
        },
        {
          name: "variant",
          type: "relationship",
          relationTo: "product-variants",
          required: false,
          filterOptions: ({ siblingData }) => {
            // siblingData contains other fields in the same row
            if (siblingData?.product) {
              return {
                product: {
                  equals: siblingData.product,
                },
              };
            }
            return {};
          },
        },
      ],
    },
    // ... other fields
  ],
}
```

The key insight is that `filterOptions` accepts a function, not just an object. This function receives an argument object with several properties, and the one we need is `siblingData`. According to Payload's documentation, the function is called with these properties:

- `siblingData` - An object containing document data scoped to fields within the same parent
- `data` - The full collection or global document being edited
- `id` - The current document ID
- `relationTo` - The collection slug to filter against
- `user` - The currently authenticated user
- `req` - The Payload Request object

For array fields specifically, `siblingData` gives you access to other fields in the same array row. When a user selects a product in `products.0.product`, the variant field at `products.0.variant` can access that selection through `siblingData.product`.

The function runs server-side when Payload constructs the API query to fetch relationship options. This means the filtering happens at the database level, and only the relevant variants are returned to the client. The dropdown shows exactly what it should.

## Understanding the Data Flow

To understand why this works, you need to grasp how Payload handles relationship fields:

When a RelationshipField component renders, it makes an API call to fetch available options from the related collection. This API call happens on the server, and Payload constructs a database query based on the field's `filterOptions`. If `filterOptions` is a static object, that filter is applied to every query. If it's a function, Payload calls that function server-side, passing in context about the current document state, and uses the returned filter object for the query.

For array fields, the `siblingData` parameter provides the current values of other fields in the same array row. This happens in real-time as the user interacts with the form. When you select a product, Payload updates the form state, and when you click on the variant dropdown, it calls the `filterOptions` function with the updated `siblingData` containing your product selection.

The crucial distinction is server-side versus client-side filtering. Custom field components run client-side and can read form state using hooks like `useFormFields`, but they cannot modify the server-side query that fetches relationship options. The `filterOptions` function runs server-side as part of query construction, which is why it works.

## Implementation Checklist

When implementing this pattern in your own Payload collections, follow these steps:

First, identify the relationship between your fields. In my case, variants belong to products through a `product` relationship field in the `product-variants` collection. The variant field in my array needs to filter based on the product field in the same row.

Second, structure your array fields with the parent field first. Place the product field before the variant field so users select the product first, which makes the UX intuitive.

Third, implement the `filterOptions` function on the dependent field. Use the pattern shown above, checking if the required sibling field has a value before returning the filter object. If the sibling field is empty, return an empty object to show all options or no options, depending on your requirements.

Fourth, test the behavior carefully. Select different products and verify the variant dropdown updates correctly. Check that the filter works when creating new documents and when editing existing ones. Test with multiple array rows to ensure each row filters independently.

## Common Pitfalls

A few mistakes can trip you up when implementing this pattern:

**Accessing the wrong data structure.** Form fields in Payload are stored flat with dot notation as keys, not nested objects. If you're debugging with `useFormFields`, remember that you access `fields['products.0.product']` not `fields.products[0].product`. This matters if you try to build custom components.

**Forgetting the function signature.** The `filterOptions` function receives a single argument object, not separate parameters. Destructure the properties you need like `({ siblingData, data, user })`.

**Returning the wrong value.** The function must return either a Where query object, `true` to not filter, or `false` to prevent all options. Returning `undefined` or `null` will cause errors. Always return an empty object `{}` if you want to show all options.

**Not handling empty state.** Check if the sibling field has a value before using it in your filter. If the user hasn't selected a product yet, `siblingData.product` will be undefined, and you need to handle that gracefully.

## Extending the Pattern

This pattern works for any relationship filtering scenario in array fields. You can filter based on multiple sibling fields by checking multiple values in `siblingData`. You can combine static filters with dynamic filters by merging filter objects. You can even access nested data or perform complex logic before returning the filter.

For example, you could filter based on a select field's value:

```typescript
filterOptions: ({ siblingData }) => {
  if (siblingData?.category === 'electronics') {
    return {
      type: { in: ['phone', 'tablet', 'laptop'] }
    };
  }
  return {};
}
```

Or combine multiple conditions:

```typescript
filterOptions: ({ siblingData, data }) => {
  const filters = {};

  if (siblingData?.product) {
    filters.product = { equals: siblingData.product };
  }

  if (data?.category) {
    filters.category = { equals: data.category };
  }

  return filters;
}
```

The key is understanding that this function runs server-side with full access to the document context, so you can implement any filtering logic your application needs.

## Conclusion

Dynamic relationship filtering in Payload CMS array fields requires using `filterOptions` as a function with `siblingData`, not custom client-side components. This server-side approach ensures filters are applied during the API query, giving users exactly the options they should see based on their selections in sibling fields.

You now know how to implement this pattern, understand why client-side approaches fail, and have working code for server-side filtering. This technique solves the common problem of relationship dropdowns showing all options when they should be filtered based on user context.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija

## LLM Response Snippet
```json
{
  "goal": "Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips",
  "responses": [
    {
      "question": "What does the article \"How to Dynamically Filter Payload CMS Relationship Fields Based on Sibling Data in Array Fields\" cover?",
      "answer": "Tutorial on dynamically filtering Payload CMS relationship fields in array rows with server-side filterOptions, siblingData context, debugging tips"
    }
  ]
}
```