---
title: Migrate to the new collections model
description: >-
  Learn how to migrate your app to the new collections model in the GraphQL
  Admin API 2026-07.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/product-merchandising/products-and-collections/migrate-to-flexible-collections
  md: >-
    https://shopify.dev/docs/apps/build/product-merchandising/products-and-collections/migrate-to-flexible-collections.md
---

# Migrate to the new collections model

The `2026-07` version of the GraphQL Admin API replaces the split between custom (manual) and smart (automated) collections with a single, composable model. Every `Collection` is now made up of one or more sources.

Each source defines which products belong to a collection. It can use strongly typed conditions, manual selections, exclusions, or a reference to another collection. A source authored by an app can be made shareable across many collections at once.

This guide shows you what's changing, how to map your existing reads and writes to the new model, and how to adopt the new capabilities that the model makes possible.

***

## Requirements

* Your app can make [authenticated requests](https://shopify.dev/docs/api/admin-graphql#authentication) to the `2026-07` version of the GraphQL Admin API or higher.
* Your app has the `read_products` and `write_products` [access scopes](https://shopify.dev/docs/api/usage/access-scopes) for any read or write operations on collections.
* You're familiar with the existing [`Collection`](https://shopify.dev/docs/api/admin-graphql/latest/objects/Collection) object and the [`collectionCreate`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionCreate) and [`collectionUpdate`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionUpdate) mutations.

***

## How it works

In API versions before `2026-07`, every collection is either a custom collection (manual product list) or a smart collection (rules in a single `ruleSet`). Mutations like [`collectionAddProducts`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionAddProducts) and [`collectionRemoveProducts`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionRemoveProducts) only work on custom collections, and rules use a generic `column`/`relation`/`condition` shape with string values.

In `2026-07`, those distinctions go away:

* A collection has one or more sources on the new `Collection.sources` field. Each source carries an `id`, `title`, and optional `description`, and a source published by an app exposes that app through [`CollectionSource.app: App`](https://shopify.dev/docs/api/admin-graphql/2026-07/interfaces/CollectionSource).
* Each source is either a [`CollectionConditionsSource`](https://shopify.dev/docs/api/admin-graphql/2026-07/objects/CollectionConditionsSource) (typed conditions and manual selections targeting products or variants, plus exclusions by product or collection) or a [`CollectionSubCollectionsSource`](https://shopify.dev/docs/api/admin-graphql/2026-07/objects/CollectionSubCollectionsSource), whose membership is inherited from other collections.
* A conditions-based source operates at one of two granularities, which you set through [`targetType`](https://shopify.dev/docs/api/admin-graphql/2026-07/enums/CollectionSourceTargetType): `PRODUCTS` (the entire product is included when any of its variants satisfies the conditions) or `VARIANTS` (only the variants that satisfy the conditions are included). Variant-level sources enable variant-precise collection pages, back-office segments, and storefront filters. Conditions that target products behave the same under either target type.
* Conditions are typed. Every supported attribute, like product tag, product status, variant price, variant inventory, or any metafield definition, has its own GraphQL type with its own `relation` enum and value definition.
* A single source can mix typed inclusion conditions, manually included products or variants, manual product exclusions, and exclusions by collection. Exclusions target products or collections, not typed attribute conditions.
* Sources can be shareable across collections. Apps can create shareable condition sources using [`collectionConditionsSourceCreate`](https://shopify.dev/docs/api/admin-graphql/2026-07/mutations/collectionConditionsSourceCreate), then attach it to one or more collections through [`CollectionShareableSourceInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionShareableSourceInput). `CollectionConditionsSource` exposes `shareable: Boolean!` so clients can tell shareable condition sources from collection-scoped ones.
* The `collectionCreate` and `collectionUpdate` mutations each gain a new `collection` input argument that takes [`CollectionCreateInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionCreateInput) and [`CollectionUpdateInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionUpdateInput) respectively. The legacy `input: CollectionInput!` argument is deprecated.

Older API versions continue to read and write legacy-shaped collections without change. However, they can't read collections that use any new feature, such as multiple sources, exclusions, sub-collection sources, or variant-targeted sources. Those collections return `404 Not Found` to older versions.

### What's new at a glance

| Old | New |
| - | - |
| `Collection.ruleSet` | `Collection.sources` |
| `CollectionRule { column, relation, condition }` | Typed `CollectionSourceInclusionCondition*` with `relation` and `values` |
| `disjunctive: Boolean` | `inclusion.matchType: ALL` or `ANY` |
| `CollectionInput.products` (create only, no rules) | `inclusion.selections: [{ productId, variantIds }]` (always available) |
| [`collectionAddProducts`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionAddProducts) and [`collectionRemoveProducts`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionRemoveProducts) | `inclusion.selectionsToAdd` and `selectionsToRemove` on `collectionUpdate` |
| `collectionCreate(input: CollectionInput!)` | `collectionCreate(collection: CollectionCreateInput)` |
| `collectionUpdate(input: CollectionInput!)` | `collectionUpdate(collection: CollectionUpdateInput)` |
| `collectionRulesConditions` query | GraphQL introspection on the typed condition types; [`collectionConditionMetafieldDefinitions`](https://shopify.dev/docs/api/admin-graphql/2026-07/queries/collectionConditionMetafieldDefinitions) query for metafields that can be used as collection conditions |
| `collection_type` filter on the `collections` query | Removed. All collections behave the same way. |
| In Functions, `Product.inAnyCollection` and `Product.inCollections` only | In Functions, you can now use `ProductVariant.inAnyCollection` and `ProductVariant.inCollections` in addition to the existing product-level fields |

### What stays the same

* `Collection.products` and pagination over products.
* Collection metadata: `image`, `seo`, `metafields`, `publications`, `sortOrder`, `templateSuffix`.
* Storefront theme APIs.

***

## Step 1: Stop branching on collection type

In `2026-07`, the same collection can have rules, manually included products, exclusions, and multiple sources at the same time. Any code that assumes custom (manual) and smart (automated) collections are separate types is no longer valid. For example, code that asserts "smart collections have no manual products" or code that determines which operations are allowed by checking `Collection.ruleSet` to see whether a collection is smart or custom.

Audit your code for:

* Branching on the presence of `Collection.ruleSet`.
* Filtering with the `collection_type` argument on the [`collections`](https://shopify.dev/docs/api/admin-graphql/latest/queries/collections) query. The argument is removed in `2026-07`.
* Pre-checking "is this a smart collection?" before calling `collectionAddProducts` or `collectionRemoveProducts`. The new model permits manual selections on conditions-based sources.

***

## Step 2: Move reads from `ruleSet` to `sources`

Replace any read of [`Collection.ruleSet`](https://shopify.dev/docs/api/admin-graphql/latest/objects/Collection#field-Collection.fields.ruleSet) with a query against `Collection.sources`. Always select `__typename` on `sources` and on every conditions array, and use aliases or fragments to read fields that are generic to each source or condition type.

## Moving a collection read

##### Latest (2026-07)

```graphql
query CollectionSources($id: ID!) {
    collection(id: $id) {
      id
      title
      sources {
        __typename
        id
        title
        ... on CollectionConditionsSource {
          targetType
          shareable
          inclusion {
            matchType
            conditions {
              __typename
              id
              ... on CollectionSourceInclusionConditionProductTag {
                tagRelation: relation
                tagValues: values
                tagMatchType: matchType
              }
              ... on CollectionSourceInclusionConditionVariantPrice {
                priceRelation: relation
                value { amount currencyCode }
              }
            }
            selections(first: 50) {
              nodes {
                product { id title }
                variantIds
              }
            }
          }
        }
        ... on CollectionSubCollectionsSource {
          collections { id title }
        }
      }
      products(first: 50) {
        nodes { id title }
      }
    }
  }
```

##### Legacy (2026-04 and earlier)

```graphql
query CollectionRuleSet($id: ID!) {
  collection(id: $id) {
    id
    title
    ruleSet {
      appliedDisjunctively
      rules {
        column
        relation
        condition
      }
    }
    products(first: 50) {
      nodes { id title }
    }
  }
}
```

Keep in mind:

* New condition types might be added in any future API version. Your client receives them as [`CollectionSourceInclusionConditionUnknown`](https://shopify.dev/docs/api/admin-graphql/2026-07/objects/CollectionSourceInclusionConditionUnknown). Decide ahead of time how your UI handles unknown conditions.

***

## Step 3: Replace `collectionRulesConditions` with introspection

The [`collectionRulesConditions`](https://shopify.dev/docs/api/admin-graphql/latest/queries/collectionRulesConditions) query previously described a partial view of a rule's schema at runtime. The new typed condition system encodes this logic into the GraphQL types themselves, so the `collectionRulesConditions` query is deprecated in `2026-07`. If you generate UI from the supported conditions, switch to GraphQL introspection on the typed condition types and their `@oneOf` input objects.

Shops can also define metafield conditions that can be used as smart collection rules. Use the new [`collectionConditionMetafieldDefinitions`](https://shopify.dev/docs/api/admin-graphql/2026-07/queries/collectionConditionMetafieldDefinitions) query to retrieve them.

***

## Step 4: Update the `collectionCreate` and `collectionUpdate` arguments

`collectionCreate` and `collectionUpdate` each accept a `collection:` argument. The legacy `input: CollectionInput!` argument is deprecated.

If you only need to update legacy fields, like `title`, `descriptionHtml`, `image`, or `seo`, the upgrade is a simple rename:

## Updating a collection's title

##### Latest (2026-07)

```graphql
mutation UpdateTitle($id: ID!, $title: String!) {
  collectionUpdate(collection: { id: $id, title: $title }) {
    collection { id title }
    userErrors { field message }
  }
}
```

##### Legacy (2026-04 and earlier)

```graphql
mutation UpdateTitle($id: ID!, $title: String!) {
  collectionUpdate(input: { id: $id, title: $title }) {
    collection { id title }
    userErrors { field message }
  }
}
```

To author sources, use [`CollectionCreateInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionCreateInput) and [`CollectionUpdateInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionUpdateInput).

***

## Step 5: Replace `collectionAddProducts` and `collectionRemoveProducts`

Products are now added to and removed from sources, not from collections, using the `selectionsToAdd` and `selectionsToRemove` deltas on `inclusion`. The same pattern works on `exclusion` to manage manual exclusions.

## Adding products to a source

##### Latest (2026-07)

```graphql
mutation AddSelections(
  $collectionId: ID!,
  $sourceId: ID!,
  $selections: [CollectionInclusionProductSelectionInput!]!
) {
  collectionUpdate(collection: {
    id: $collectionId,
    sourcesToUpdate: [{
      condition: {
        id: $sourceId,
        inclusion: { selectionsToAdd: $selections }
      }
    }]
  }) {
    collection { id }
    userErrors { field message }
  }
}
```

##### Legacy (2026-04 and earlier)

```graphql
mutation AddProducts($collectionId: ID!, $productIds: [ID!]!) {
  collectionAddProducts(id: $collectionId, productIds: $productIds) {
    collection { id }
    userErrors { field message }
  }
}
```

A [`CollectionInclusionProductSelectionInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionInclusionProductSelectionInput) is `{ productId: ID!, variantIds: [ID!] }`. Pass `variantIds` only when you're authoring a variant-scoped source. To remove products, send the same selections through `selectionsToRemove`.

***

## Step 6: Author sources directly when creating a collection

A new collection can include conditions, manual selections, and exclusions in a single mutation. The sources you provide on `CollectionCreateInput` are typed through the `@oneOf` input [`CollectionCreateSourceTargetInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionCreateSourceTargetInput): pass exactly one of `source` (a new conditions-based source), `subCollections` (a sub-collection source), or `shareableSource` (a reference to an existing shareable source).

## Create a collection with rules and a manual override

```graphql
mutation Create($input: CollectionCreateInput!) {
  collectionCreate(collection: $input) {
    collection { id title }
    userErrors { field message }
  }
}
```

#### Conditions source

## Variables

```json
{
  "input": {
    "title": "Vegan, but feature these",
    "sources": [
      {
        "source": {
          "title": "Vegan tag",
          "targetType": "PRODUCTS",
          "inclusion": {
            "matchType": "ANY",
            "conditions": [
              { "productTag": { "relation": "TAGGED_WITH", "values": ["vegan"], "matchType": "ANY" } }
            ],
            "selections": [
              { "productId": "gid://shopify/Product/123" }
            ]
          },
          "exclusion": {
            "conditions": [
              { "collection": { "values": ["gid://shopify/Collection/4"] } }
            ]
          }
        }
      }
    ]
  }
}
```

#### Sub-collections source

## Variables

```json
{
  "input": {
    "title": "Featured collections",
    "sources": [
      {
        "subCollections": {
          "title": "Featured snowboards",
          "collectionIds": ["gid://shopify/Collection/2"]
        }
      }
    ]
  }
}
```

#### Shareable source

## Variables

```json
{
  "input": {
    "title": "Seasonal picks",
    "sources": [
      {
        "shareableSource": {
          "sourceId": "gid://shopify/CollectionConditionsSource/1"
        }
      }
    ]
  }
}
```

***

## Step 7: Update existing conditions with the delta API

[`CollectionUpdateSourceInclusionInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionUpdateSourceInclusionInput) and [`CollectionUpdateSourceExclusionInput`](https://shopify.dev/docs/api/admin-graphql/2026-07/input-objects/CollectionUpdateSourceExclusionInput) expose `conditionsToCreate`, `conditionsToUpdate`, and `conditionsToDelete`, each addressed by condition `id`.

## Add and remove conditions in a single mutation

```graphql
mutation EditConditions(
  $collectionId: ID!,
  $sourceId: ID!,
  $newConditions: [CollectionSourceInclusionConditionInput!]!,
  $obsoleteConditionIds: [ID!]!
) {
  collectionUpdate(collection: {
    id: $collectionId,
    sourcesToUpdate: [{
      condition: {
        id: $sourceId,
        inclusion: {
          conditionsToCreate: $newConditions,
          conditionsToDelete: $obsoleteConditionIds
        }
      }
    }]
  }) {
    collection { id }
    userErrors { field message }
  }
}
```

***

## Step 8: Move Function input queries to read from `ProductVariant`

If your app ships a [Shopify Function](https://shopify.dev/docs/apps/build/functions) that uses collections, like a discount, cart transform, cart validation, or delivery customization, your input query must read collection membership from [`ProductVariant`](https://shopify.dev/docs/api/admin-graphql/2026-07/objects/ProductVariant), not [`Product`](https://shopify.dev/docs/api/admin-graphql/2026-07/objects/Product).

In `2026-07`, `ProductVariant` exposes the same membership fields as `Product`:

* `inAnyCollection(ids: [ID!]!): Boolean!`
* `inCollections(ids: [ID!]!): [CollectionMembership!]!`

`Product.inAnyCollection` and `Product.inCollections` continue to work for backward compatibility, but they only check whether a product in its entirety (all variants) is included in the collection. On a variant-scoped collection, they return `false` for every variant. Read from the variant instead.

## A Function input query

##### Latest (2026-07)

```graphql
query Input($collectionIds: [ID!]!) {
  cart {
    lines {
      id
      merchandise {
        __typename
        ... on ProductVariant {
          id
          inAnyCollection(ids: $collectionIds)
          inCollections(ids: $collectionIds) {
            collectionId
            isMember
          }
        }
      }
    }
  }
}
```

##### Legacy (2026-04 and earlier)

```graphql
query Input($collectionIds: [ID!]!) {
  cart {
    lines {
      id
      merchandise {
        __typename
        ... on ProductVariant {
          id
          product {
            id
            inAnyCollection(ids: $collectionIds)
            inCollections(ids: $collectionIds) {
              collectionId
              isMember
            }
          }
        }
      }
    }
  }
}
```

You don't need to register a collection for materialization. All collections referenced from Function input queries are materialized for you.

***

## Migration checklist

Before treating your migration as complete:

* You've removed any branching on the legacy collection type.
* You've replaced `Collection.ruleSet` reads with `Collection.sources` and inline fragments.
* You've handled [`CollectionSourceInclusionConditionUnknown`](https://shopify.dev/docs/api/admin-graphql/2026-07/objects/CollectionSourceInclusionConditionUnknown) in your UI.
* You've moved `collectionAddProducts` and `collectionRemoveProducts` calls to `selectionsToAdd` and `selectionsToRemove`.
* You've renamed `collectionCreate(input:)` and `collectionUpdate(input:)` to `collectionCreate(collection:)` and `collectionUpdate(collection:)`.
* You've replaced `collectionRulesConditions` with GraphQL introspection.
* You've stopped passing `collection_type` to the [`collections`](https://shopify.dev/docs/api/admin-graphql/latest/queries/collections) query.
* You've updated Function or discount input queries to read membership from `ProductVariant`.
* You've added a friendly fallback for collections that return `404` because they use new features unavailable in an older API version that your client also supports.

***

## Next steps

* Learn how to [use the new collections model](https://shopify.dev/docs/apps/build/product-merchandising/products-and-collections/use-new-collections-model).
* Browse the [`Collection`](https://shopify.dev/docs/api/admin-graphql/2026-07/objects/Collection) reference to explore every new field.
* Try mutations against your dev store with the [GraphiQL explorer](https://shopify.dev/docs/api/usage/api-exploration/admin-graphiql-explorer).

***
