Skip to main content

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.



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 and 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.
  • Each source is either a CollectionConditionsSource (typed conditions and manual selections targeting products or variants, plus exclusions by product or collection) or a CollectionSubCollectionsSource, whose membership is inherited from other collections.
  • A conditions-based source operates at one of two granularities, which you set through targetType: 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, then attach it to one or more collections through 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 and 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.

Anchor to What's new at a glanceWhat's new at a glance

OldNew
Collection.ruleSetCollection.sources
CollectionRule { column, relation, condition }Typed CollectionSourceInclusionCondition* with relation and values
disjunctive: Booleaninclusion.matchType: ALL or ANY
CollectionInput.products (create only, no rules)inclusion.selections: [{ productId, variantIds }] (always available)
collectionAddProducts and collectionRemoveProductsinclusion.selectionsToAdd and selectionsToRemove on collectionUpdate
collectionCreate(input: CollectionInput!)collectionCreate(collection: CollectionCreateInput)
collectionUpdate(input: CollectionInput!)collectionUpdate(collection: CollectionUpdateInput)
collectionRulesConditions queryGraphQL introspection on the typed condition types; collectionConditionMetafieldDefinitions query for metafields that can be used as collection conditions
collection_type filter on the collections queryRemoved. All collections behave the same way.
In Functions, Product.inAnyCollection and Product.inCollections onlyIn Functions, you can now use ProductVariant.inAnyCollection and ProductVariant.inCollections in addition to the existing product-level fields

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

Anchor to Step 1: Stop branching on collection typeStep 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 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.

Replace any read of Collection.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

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 }
}
}
}
query CollectionRuleSet($id: ID!) {
collection(id: $id) {
id
title
ruleSet {
appliedDisjunctively
rules {
column
relation
condition
}
}
products(first: 50) {
nodes { id title }
}
}
}

Keep in mind:


Anchor to Step 3: Replace ,[object Object], with introspectionStep 3: Replace collectionRulesConditions with introspection

The 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 query to retrieve them.


Anchor to Step 4: Update the ,[object Object], and ,[object Object], argumentsStep 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

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

To author sources, use CollectionCreateInput and CollectionUpdateInput.


Anchor to Step 5: Replace ,[object Object], and ,[object Object]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

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

A 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.


Anchor to Step 6: Author sources directly when creating a collectionStep 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: 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

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

Variables

{
"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"] } }
]
}
}
}
]
}
}

Anchor to Step 7: Update existing conditions with the delta APIStep 7: Update existing conditions with the delta API

CollectionUpdateSourceInclusionInput and CollectionUpdateSourceExclusionInput expose conditionsToCreate, conditionsToUpdate, and conditionsToDelete, each addressed by condition id.

Add and remove conditions in a single mutation

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 }
}
}

Anchor to Step 8: Move Function input queries to read from ,[object Object]Step 8: Move Function input queries to read from ProductVariant

If your app ships a Shopify Function that uses collections, like a discount, cart transform, cart validation, or delivery customization, your input query must read collection membership from ProductVariant, not 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

query Input($collectionIds: [ID!]!) {
cart {
lines {
id
merchandise {
__typename
... on ProductVariant {
id
inAnyCollection(ids: $collectionIds)
inCollections(ids: $collectionIds) {
collectionId
isMember
}
}
}
}
}
}
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.


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 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 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.


Was this page helpful?