Get a peek at our latest innovations at Sanity Product Day on Dec 8th →

Content Calendar

Official(made by Sanity team)
v3 Ready

By Rune Botten & Fred Carlsen

Schedule and view your editorial calendar, right where you store your content. Prioritize and get organized on the fly with a visual calendar in your Studio.

Sanity Plugin Content Calendar

Schedule and view your editorial calendar, right where you store your content. Prioritize and get organized on the fly with a visual calendar in your Studio.

Sanity Content Calendar in Studio


  • Graphical representation of your editorial calendar
  • At-a-glance schedule review
  • Quick prioritization of content to review
  • More time to create content, less time spent scheduling

Table of contents


Run the following command in your studio folder using the Sanity CLI:

sanity install content-calendar


To use the calendar, the plugin needs to know which document types to display and what fields to use for scheduling documents.

Create or open the config file found in config/content-calendar.json. The file is automatically created the first time the studio starts after adding the plugin. Add your document type and field combinations.

  "types": [
      "type": "post",
      "field": "publishedAt",
      "titleField": "title"
  "calendar": {
    "event": {
      "dateFormat": "MMMM, dd yyyy",
      "timeFormat": "hh:mm a",
      "showAuthor": "false"
  "filterWarnings": []

titleField also supports nested properties, like title.en.

In the configuration values, you can also modify how the dates and times are formatted on the calendar, as well as being able to show the document author.

Note: the type.field option signals when this post should be scheduled to release, this requires us to add a "date" or "datetime" field to the document you want to enable scheduling for.

If edits are made to a Document after it has been Scheduled, a Warning will show. However, if you want to hide this, use the filterWarnings key. This will evaluate the Document for a matching condition, so for example:

"filterWarnings": [
  {"_type": "article", "title.en": "Hello!"},
  {"isLive": true},

If an Event matches all of the conditions in an any of the Objects in the Array, the Warning will be hidden.

In the example above:

  • A document with the type article AND a title.en value of Hello! will not show Warnings OR
  • Any document with an isLive field value of true will not show Warnings

Installing with other custom document actions

The plugin adds the Schedule, Unschedule, and Reschedule actions to your configured documents by implementing the part part:@sanity/base/document-actions/resolver.

Because of a current limitation (as of version 2.0.9), these will not compose with your own implementation of this part for resolving document actions. For more on the parts system, see the part system documentation.

To implement the plugin, add the custom scheduling actions with your custom action.

// import the default document actions
import defaultResolve from 'part:@sanity/base/document-actions'
import {addActions} from 'sanity-plugin-content-calendar/build/register'

const CustomAction = () => ({
  label: 'Hello world',
  onHandle: () => {
    window.alert('👋 Hello from custom action')

export default function resolveDocumentActions(props) {
  const actions = [...defaultResolve(props), CustomAction]
  return addActions(props, actions)

Installing with other custom document badges

Much like custom document actions, if you have implemented custom document badges with part:@sanity/base/document-badges/resolver you need to add in the Scheduled badge from the plugin.

import defaultResolve from 'part:@sanity/base/document-badges'
import {addBadge} from 'sanity-plugin-content-calendar/build/register'

const CustomBadge = () => {
  return {
    label: 'Custom',
    title: 'Hello I am a custom document badge',
    color: 'success'

export default function resolveDocumentBadges(props) {
  const badges = [...defaultResolve(props), CustomBadge]
  return addBadge(props, badges)


Performing the publish event, in the future

This plugin does not perform the publishing of documents on its own, as it is just a Studio plugin running in an editors' browser. In order to actually perform the scheduled publishing, a script needs to run either periodically, or at the given publishing times to perform the publish action.

We advise setting up a cronjob running for instance every minute that checks if any document should be published and then perform that action. A full script that does this is represented below.

When the publish event eventually occurs, any newer draft will be discarded. This is why the plugin warns you if you make further changes to a document after you schedule it. If you don't want to lose the newer changes an editor will need to Reschedule them. The plugin will prompt for this with a warning in the calendar view and an updated document action.

Publishing the scheduled documents

To publish documents, you can set up a serverless function to poll for pending scheduled events and perform the action. Typically, this can be run from a cronjob every minute or from another scheduled action.

Alternatively, you could schedule this script to run at specific times by using webhooks and listening for new schedule.metadata documents.

const sanityClient = require('@sanity/client')
const client = sanityClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  // Need a write token in order to read schedule metadata and publish documents
  token: 'your-write-token',
  useCdn: false

// Query for any scheduled publish events that should occur
const query = `* [_type == "schedule.metadata" && !(_id in path("drafts.**")) && datetime <= now()]`

const publish = async (metadata, client) => {
  const dataset = client.config().dataset
  const id = metadata.documentId
  const rev = metadata.rev

  // Fetch the draft revision we should publish from the History API
  const uri = `/data/history/${dataset}/documents/drafts.${id}?revision=${rev}`
  const revision = await client
    .then(response => response.documents.length && response.documents[0])

  if (!revision) {
    // Here we have a situation where the scheduled revision does not exist
    // This can happen if the document was deleted via Studio or API without
    // unscheduling it first.
    console.error('Could not find document revision to publish', metadata)

  // Publish it
  return (
      // Publishing a document is simply writing it to the dataset without a
      // `drafts.` prefix. The `documentId` field on the metadata already does
      // not include this prefix, but the revision we fetched probably does, so
      // we overwrite it here.
      .createOrReplace(Object.assign({}, revision, {_id: id}))
      // Then we delete any current draft.
      // And finally we delete the schedule medadata, since we're done with it.

  .then(response => Promise.all( => publish(metadata, client))))

Studio Version

This plugin is available for Studio v3 and Studio v2.What are studio versions and which do I choose?


Other plugins by authors

Mux input
- has 7 likes

v3 Ready

Upload videos to Mux directly from the studio.

Go to Mux input


Adds the Bynder Digital Asset Management system to your Studio

Rune Botten
Go to Bynder

Google Translate
- has 7 likes

Official(made by Sanity team)

Machine translations from Google Translate directly in your Sanity Studio

Rune Botten
Go to Google Translate