Common patterns for the Content Releases API
Query and modify Content Releases using the API
Interfacing with Content Releases is similar to interfacing with other documents and relationships in the Content Lake.
Prerequisites:
- The examples use several APIs. Releases aren't public, so requests must be authenticated with a token that has permission to read unpublished content, and must use the correct URL format for each API.
- Release APIs and features are available in API version
2025-02-19and later unless otherwise noted. - Queries that read release information need a perspective that can see it. Set the perspective to
raw, or to a stack of release names. See Perspectives for Content Lake. - The
@sanity/clientexamples require version 7.8.0 or later, which is whencreateVersiongainedbaseId. See Content Releases and versions with @sanity/client.
Examples that don't construct a client assume the configured client shown above.
Create a new release
Releases are Sanity documents of type system.release, so you can mutate them like any other document. The Sanity client also offers helper methods:
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
})
const {releaseId} = await client.releases.create({
metadata: {
title: 'New bike release',
releaseType: 'asap',
},
})For the full list of metadata properties, see the type definitions in your editor or Create a new release in the Actions API guide.
Create a document version in a release
Use client.createVersion() to add a version of an existing published document to a release. Prefer baseId so Sanity copies the current published content for you. See Content Releases and versions with @sanity/client for the full guide.
await client.createVersion({
releaseId,
publishedId: 'product-123',
baseId: 'product-123',
})Modify release information
To change a release's metadata, pass a patch along with the releaseId:
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
})
const release = await client.releases.edit({
releaseId: 'RELEASE_NAME',
patch: {
set: {
metadata: {
releaseType: 'asap',
},
},
},
})You can also edit releases using the Mutate API.
Get all releases for a project and dataset
Query release documents with the releases::all() GROQ function. This is the preferred way to list releases.
JS client
Use the client's fetch method to query for releases.
Input
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
perspective: 'raw',
})
const releases = await client.fetch('releases::all()')
console.log(releases)Response
[
{
"_createdAt": "2024-11-26T21:30:57Z",
"finalDocumentStates": null,
"_updatedAt": "2024-12-17T16:33:26Z",
"_type": "system.release",
"name": "rHw6FBu82",
"_id": "_.releases.rHw6FBu82",
"state": "active",
"metadata": {
"releaseType": "scheduled",
"title": "End of year release",
"intendedPublishAt": "2024-12-30T08:00:00Z"
},
"publishAt": "2024-12-30T08:00:00Z",
"_rev": "JmI5JuFTDPq3paS6p09Jmu",
"userId": "paATypsg4"
},
{
"publishAt": null,
"_rev": "1kbjGQwz5Z0FmijO2l7Lwl",
"finalDocumentStates": [
{
"id": "versions.rglJO3Sfg.movie_70981",
"_key": "1kbjGQwz5Z0FmijO2l7M0E"
}
],
"_id": "_.releases.rglJO3Sfg",
"state": "published",
"metadata": {
"title": "Quick fixes",
"releaseType": "asap"
},
"_createdAt": "2024-11-26T22:01:56Z",
"_type": "system.release",
"name": "rglJO3Sfg",
"_updatedAt": "2024-12-02T17:32:59Z",
"userId": ""
},
{
"_createdAt": "2024-11-26T18:34:14Z",
"name": "rqZSzJ1uS",
"finalDocumentStates": [
{
"id": "versions.rqZSzJ1uS.movie_10681"
}
],
"userId": "paATypsg4",
"_id": "_.releases.rqZSzJ1uS",
"state": "published",
"_updatedAt": "2024-12-05T17:22:00Z",
"metadata": {
"releaseType": "scheduled",
"description": "Experimental updates for testing",
"title": "Experimental updates",
"intendedPublishAt": "2024-12-05T17:22:00.000Z"
},
"publishAt": "2024-12-05T17:22:00Z",
"_rev": "5dKCVUpSDccCmU1E23aUAc",
"_type": "system.release"
}
// ...
]Query API
Use the same releases::all() function through the Query API.
Use the following GROQ query in the API request:
releases::all()
Input
curl "https://YOUR_PROJECT_ID.api.sanity.io/v2025-02-19/data/query/production?query=releases%3A%3Aall()&perspective=raw" \ -H "Authorization: Bearer $SANITY_API_TOKEN"
Example response
{
"query": "releases::all()",
"result": [
{
"_createdAt": "2024-11-26T21:30:57Z",
"finalDocumentStates": null,
"_updatedAt": "2024-12-17T16:33:26Z",
"_type": "system.release",
"name": "rHw6FBu82",
"_id": "_.releases.rHw6FBu82",
"state": "active",
"metadata": {
"releaseType": "scheduled",
"title": "End of year release",
"intendedPublishAt": "2024-12-30T08:00:00Z"
},
"publishAt": "2024-12-30T08:00:00Z",
"_rev": "JmI5JuFTDPq3paS6p09Jmu",
"userId": "paATypsg4"
},
{
"publishAt": null,
"_rev": "1kbjGQwz5Z0FmijO2l7Lwl",
"finalDocumentStates": [
{
"id": "versions.rglJO3Sfg.movie_70981",
"_key": "1kbjGQwz5Z0FmijO2l7M0E"
}
],
"_id": "_.releases.rglJO3Sfg",
"state": "published",
"metadata": {
"title": "Quick fixes",
"releaseType": "asap"
},
"_createdAt": "2024-11-26T22:01:56Z",
"_type": "system.release",
"name": "rglJO3Sfg",
"_updatedAt": "2024-12-02T17:32:59Z",
"userId": ""
},
{
"_createdAt": "2024-11-26T18:34:14Z",
"name": "rqZSzJ1uS",
"finalDocumentStates": [
{
"id": "versions.rqZSzJ1uS.movie_10681"
}
],
"userId": "paATypsg4",
"_id": "_.releases.rqZSzJ1uS",
"state": "published",
"_updatedAt": "2024-12-05T17:22:00Z",
"metadata": {
"releaseType": "scheduled",
"description": "Experimental updates for testing",
"title": "Experimental updates",
"intendedPublishAt": "2024-12-05T17:22:00.000Z"
},
"publishAt": "2024-12-05T17:22:00Z",
"_rev": "5dKCVUpSDccCmU1E23aUAc",
"_type": "system.release"
}
],
"syncTags": [
"s1:r6H+EQ"
],
"ms": 3
}To view only active releases, and exclude archived releases, adjust your GROQ query to compare the state property.
releases::all()[state == 'active']
Get all documents from a release
Query all documents associated with a release.
JS client
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
})
const {result: documents} = await client.releases.fetchDocuments({
releaseId: 'RELEASE_NAME',
})
console.log(documents)fetchDocuments returns the release's document versions. To filter or project them in a query instead, use the sanity::partOfRelease GROQ function.
sanity::partOfRelease GROQ function
The sanity::partOfRelease GROQ function accepts a release name and returns every document in that release.
Release ID vs. release name
A release name is the final segment of a release ID. A release with an _id of _.releases.rEGM2JqQ3 has the name rEGM2JqQ3. Use only that final segment when referencing a release by name.
Use the function in a GROQ query and pass the release name string.
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
perspective: 'raw',
})
const query = "*[sanity::partOfRelease($releaseName)]{ _id }"
const params = {releaseName: 'RELEASE_NAME'}
const documents = await client.fetch(query, params)
console.log(documents)Get all versions of a document
Query all versions (published, drafts, and release versions) of a document.
sanity::versionOf GROQ function
The sanity::versionOf GROQ function accepts a document ID and returns all versions of a document.
Use the function in a GROQ query and pass the document ID string.
import {createClient} from '@sanity/client'
import {getPublishedId} from 'sanity'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
perspective: 'raw',
})
const query = "*[sanity::versionOf($publishedId)]{ _id }"
const documentId = 'versions.rABC123.movie_70981'
const params = {publishedId: getPublishedId(documentId)}
const versions = await client.fetch(query, params)
console.log(versions)Gotcha
The function expects a published document ID. For example: abc123 is acceptable, but drafts.abc123 and versions.r1324.abc123 are not. You can use the getPublishedId helper imported from sanity to derive it from any Id.
The function also works with the Query API and anywhere that supports GROQ functions.
Doc API
Use the document ID, along with the Doc API endpoint to retrieve all versions of a specific document. The includeAllVersions boolean query parameter returns all versions for the document.
Input
GET /v2025-02-19/data/doc/production/movie_70981?includeAllVersions=true
Example response
{
"documents": [
{
"_createdAt": "2018-06-13T08:57:45Z",
"_id": "movie_70981",
"_rev": "1kbjGQwz5Z0FmijO2l7Lwl",
"_type": "movie",
"_updatedAt": "2024-12-02T17:32:59Z"
// ...
},
{
"_createdAt": "2018-06-13T08:57:45Z",
"_id": "drafts.movie_70981",
"_rev": "3276f9d1-0343-4b78-a79e-8c6561942f1b",
"_type": "movie",
"_updatedAt": "2024-12-02T17:14:27Z"
// ...
},
{
"_createdAt": "2018-06-13T08:57:45Z",
"_id": "versions.rHw6FBu82.movie_70981",
"_rev": "a000fa99-072c-434c-8669-825103a111b7",
"_type": "movie",
"_updatedAt": "2024-11-27T18:36:19Z"
// ...
}
],
"omitted": []
}Use releases in perspective queries
Content Releases use a layering system that layers document versions atop one another, allowing you to create a custom perspective stack. You can learn more about layering in the Content Releases User Guide.
Besides raw, published, and drafts, the perspective parameter accepts a comma-separated list of release names. Releases take priority from left to right.
For example, in the perspective a,b,c you would see changes in a take priority over b and c, and changes in b take priority over c.
The published perspective is automatically added to the end, so even if a release only contains changes to one document, the response will include all matching published documents in addition to the release changes.
Query API
To query against a list of releases, use the perspective query parameter and order the release names by priority from left to right. For example: ?perspective=a,b,c.
A GROQ query such as *[_type == 'movie']{ _id } returns the document IDs that match the release layer.
Input
GET https://YOUR_PROJECT_ID.api.sanity.io/v2025-02-19/data/query/production?query=<GROQ_QUERY>&perspective=RELEASE_NAME_1,RELEASE_NAME_2
Example response
{
"query": "*[_type == 'movie']{ _id }",
"result": [
{ "_id": "a306e7cf-ea18-4a43-8ce2-0586073c41c8" },
{ "_id": "movie_10681" },
{ "_id": "movie_118340" },
{ "_id": "movie_126889" },
{ "_id": "movie_157336" },
{ "_id": "movie_17654" }
],
"syncTags": ["s1:+jIWIw"],
"ms": 5
}JavaScript client
The client also accepts an array of release names. Priority runs left to right, so put drafts first when unpublished edits should win over release content, and last when release content should win. published is appended automatically; drafts is not.
Edit the perspective value to insert your release names.
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
perspective: ['drafts', 'RELEASE_NAME_1', 'RELEASE_NAME_2'],
})
const movies = await client.fetch("*[_type == 'movie']")
console.log(movies)Another common pattern is to extend your client configuration for releases and draft preview by creating a new client from the existing one.
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2025-02-19',
useCdn: true, // Safe here: this client only reads published content
perspective: 'published', // default
})
const previewClient = client.withConfig({
token: process.env.SANITY_API_TOKEN,
useCdn: false, // Release and draft content is never served from the CDN cache
perspective: ['drafts', 'RELEASE_NAME'],
})Trigger webhooks or functions by release state
Release documents are queryable, so they can trigger webhooks.
The release state is usually the most useful trigger:
A release reports its status on the state property. For the full list of states, including the transient states a release passes through, and the transitions between them, see Content release document flow.
Functions
You can create Sanity Functions that activate when you create a release, or when it moves through different states.
import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'
export default defineBlueprint({
resources: [
defineDocumentFunction({
name: 'release-function',
event: {
on: ['create', 'update'],
filter: '_type == "system.release"'
}
}),
],
})import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'
export default defineBlueprint({
resources: [
defineDocumentFunction({
name: 'release-function',
event: {
on: ['update'],
filter: '_type == "system.release" && delta::changedAny(state) && state == "published"'
}
}),
],
})Webhooks
To trigger on every new release, use this filter in the rule object of a GROQ-powered webhook:
{
"rule": {
"on": ["create"],
"filter": "_type == 'system.release'"
}
}To trigger only on releases entering a given state, compare state in the filter. This rule fires only when a release transitions into published:
{
"rule": {
"on": ["update"],
"filter": "_type == 'system.release' && delta::changedAny(state) && state == 'published'"
}
}Find documents marked for unpublishing
A release can mark published documents to be unpublished when it publishes. Those version documents carry _system.delete. Unpublishing removes the published document; the content stays in the dataset. Replace $releaseName with your own release name.
releases::all()[name == $releaseName] {
_id,
"docs": *[sanity::partOfRelease(^.name) && _system.delete == true]._id
}Patch all versions of a document
If you need to make an update to all versions of a document, you can use the mutations API along with the sanity::versionOf GROQ function.
await client
.patch({query: "*[sanity::versionOf($publishedId)]", params: {publishedId: 'movie_70981'}})
.set({title: 'Example title'})
.commit()curl "https://YOUR_PROJECT_ID.api.sanity.io/v2025-02-19/data/mutate/production" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SANITY_API_TOKEN" \
--data '{
"mutations": [
{
"patch": {
"query": "*[sanity::versionOf($publishedId)]",
"params": {"publishedId": "movie_70981"},
"set": {"title": "Example title"}
}
}
]
}'Query and sort releases to build a perspective stack
To build a perspective stack outside a preview or visual-editing context, query, filter, and sort the releases yourself. This example filters on _type == "system.release" directly so it can group by metadata.releaseType in one query.
This example:
- Queries releases by their type.
- Orders results by date, descending, so the furthest in the future come first. Scheduled releases use
publishAtormetadata.intendedPublishAt; other types use_createdAt. - Builds a perspective stack you can query with. Including every release behaves much like
raw, with layering differences that depend on the order.
In practice, you should adjust the criteria for which releases to include.
// This client must use the `raw` perspective and apiVersion 2025-02-19 or later.
import {client} from './client'
type Release = {name: string}
const RELEASES_QUERY = `{
"undecided": *[_type == "system.release" && state == "active" && metadata.releaseType == "undecided"] | order(_createdAt desc) {
_id,
_createdAt,
name,
metadata { title }
},
"asap": *[_type == "system.release" && state == "active" && metadata.releaseType == "asap"] | order(_createdAt desc) {
_id,
_createdAt,
name,
metadata { title }
},
"scheduled": *[_type == "system.release" && state in ["active", "scheduled"] && metadata.releaseType == "scheduled"] | order(coalesce(publishAt, metadata.intendedPublishAt) desc) {
_id,
_createdAt,
name,
publishAt,
metadata { title, intendedPublishAt }
}
}`
const {undecided, asap, scheduled}: {
undecided: Release[]
asap: Release[]
scheduled: Release[]
} = await client.fetch(RELEASES_QUERY)
// Priority runs left to right. `drafts` first means unpublished edits win over release content.
const perspectiveStack = [
'drafts',
...asap.map((release) => release.name),
...scheduled.map((release) => release.name),
...undecided.map((release) => release.name),
// `published` is appended automatically
]
const perspectiveClient = client.withConfig({perspective: perspectiveStack})Perspectives take priority from left to right. Learn more in the perspective documentation.
Get perspective stack in custom components
If you're building with Studio, you can use the usePerspective hook to retrieve the active perspective stack.
import {usePerspective} from 'sanity'
function MyComponent() {
const {perspectiveStack} = usePerspective()
// ...
}