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.
Anchor to RequirementsRequirements
- Your app can make authenticated requests to the
2026-07version of the GraphQL Admin API or higher. - Your app has the
read_productsandwrite_productsaccess scopes for any read or write operations on collections. - You're familiar with the existing
Collectionobject and thecollectionCreateandcollectionUpdatemutations.
Anchor to How it worksHow 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 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.sourcesfield. Each source carries anid,title, and optionaldescription, and a source published by an app exposes that app throughCollectionSource.app: App. - Each source is either a
CollectionConditionsSource(typed conditions and manual selections targeting products or variants, plus exclusions by product or collection) or aCollectionSubCollectionsSource, 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) orVARIANTS(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
relationenum 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 throughCollectionShareableSourceInput.CollectionConditionsSourceexposesshareable: Boolean!so clients can tell shareable condition sources from collection-scoped ones. - The
collectionCreateandcollectionUpdatemutations each gain a newcollectioninput argument that takesCollectionCreateInputandCollectionUpdateInputrespectively. The legacyinput: 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
| 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 and 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 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 |
Anchor to What stays the sameWhat stays the same
Collection.productsand 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_typeargument on thecollectionsquery. The argument is removed in2026-07. - Pre-checking "is this a smart collection?" before calling
collectionAddProductsorcollectionRemoveProducts. The new model permits manual selections on conditions-based sources.
Anchor to Step 2: Move reads from ,[object Object], to ,[object Object]Step 2: Move reads from ruleSet to sources
ruleSet to sourcesReplace 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
Latest (2026-07)
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)
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. Decide ahead of time how your UI handles unknown conditions.
Anchor to Step 3: Replace ,[object Object], with introspectionStep 3: Replace collectionRulesConditions with introspection
collectionRulesConditions with introspectionThe 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 argumentscollectionCreate 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)
mutation UpdateTitle($id: ID!, $title: String!) {
collectionUpdate(collection: { id: $id, title: $title }) {
collection { id title }
userErrors { field message }
}
}Legacy (2026-04 and earlier)
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
collectionAddProducts and collectionRemoveProductsProducts 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)
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)
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.
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
Conditions source
Variables
Sub-collections source
Variables
Shareable source
Variables
Variables
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
Anchor to Step 8: Move Function input queries to read from ,[object Object]Step 8: Move Function input queries to read from ProductVariant
ProductVariantIf 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
Latest (2026-07)
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)
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.
Anchor to Migration checklistMigration checklist
Before treating your migration as complete:
- You've removed any branching on the legacy collection type.
- You've replaced
Collection.ruleSetreads withCollection.sourcesand inline fragments. - You've handled
CollectionSourceInclusionConditionUnknownin your UI. - You've moved
collectionAddProductsandcollectionRemoveProductscalls toselectionsToAddandselectionsToRemove. - You've renamed
collectionCreate(input:)andcollectionUpdate(input:)tocollectionCreate(collection:)andcollectionUpdate(collection:). - You've replaced
collectionRulesConditionswith GraphQL introspection. - You've stopped passing
collection_typeto thecollectionsquery. - You've updated Function or discount input queries to read membership from
ProductVariant. - You've added a friendly fallback for collections that return
404because they use new features unavailable in an older API version that your client also supports.
Anchor to Next stepsNext steps
- Learn how to use the new collections model.
- Browse the
Collectionreference to explore every new field. - Try mutations against your dev store with the GraphiQL explorer.