How to use `sanity exec` to import content without browser globals?

9 replies
Last updated: Nov 9, 2022
Hello! Is there some sample code/examples/docs for how to run a script to import existing content using
sanity exec
? Everything I’ve found so far seems to be code meant to run in a browser (and when trying to import my schema I get errors looking for the
window
global)
AI Update

The "window is not defined" error you're encountering happens because sanity exec runs your script in a Node.js environment, not a browser. When you try to import schema files or other code that references browser globals like window, it fails.

Here's how to write import scripts that work with sanity exec:

Basic Import Script Structure

Create a script file (e.g., scripts/import.ts) that uses the Sanity client in a Node environment:

// scripts/import.ts
import {getCliClient} from 'sanity/cli'

const client = getCliClient()

async function importContent() {
  console.log(`Importing to:`)
  console.log(`Project ID: ${client.config().projectId}`)
  console.log(`Dataset: ${client.config().dataset}`)

  // Your import logic here
  const documents = [
    {
      _type: 'post',
      _id: 'my-post-id',
      title: 'My Post Title',
    }
  ]

  const transaction = client.transaction()
  
  documents.forEach(doc => {
    transaction.createOrReplace(doc)
  })

  await transaction.commit()
    .then(() => console.log('Import complete!'))
    .catch(err => console.error('Import failed:', err))
}

importContent()

Run it with:

npx sanity exec scripts/import.ts --with-user-token

The --with-user-token flag provides authentication using your credentials.

Avoiding Schema Import Issues

If you need to reference your schema types but get window is not defined errors, you have a few options:

Option 1: Don't import the full schema
Instead of importing schema files directly, just use plain objects that match your schema structure. The client doesn't need the schema definitions to create documents.

Option 2: Import only type definitions
If you're using TypeScript, import just the generated types (not the schema):

import type {Post} from '../sanity.types'

Option 3: Use Schema.compile for validation (as shown in the demo content guide)
If you need schema validation, compile it specifically for Node:

import {Schema} from '@sanity/schema'
import {schemaTypes} from '../schemas'

const defaultSchema = Schema.compile({types: schemaTypes})
const postSchema = defaultSchema.get('post')

Complete Working Example

Here's a more complete example that imports content with transactions in batches to avoid rate limits:

// scripts/import.ts
import {getCliClient} from 'sanity/cli'
import type {SanityDocumentLike} from 'sanity'

const client = getCliClient()

// Your source data (could come from an API, file, etc.)
const sourceData = [
  {title: 'Post 1', slug: 'post-1'},
  {title: 'Post 2', slug: 'post-2'},
  // ... more data
]

async function importContent() {
  console.log(`Starting import...`)
  
  // Transform your source data to Sanity documents
  const documents: SanityDocumentLike[] = sourceData.map(item => ({
    _type: 'post',
    _id: item.slug, // Use source ID for idempotency
    title: item.title,
  }))

  // Create in batches to avoid rate limits
  const BATCH_SIZE = 100
  for (let i = 0; i < documents.length; i += BATCH_SIZE) {
    const batch = documents.slice(i, i + BATCH_SIZE)
    const transaction = client.transaction()
    
    batch.forEach(doc => transaction.createOrReplace(doc))
    
    await transaction.commit()
    console.log(`Imported batch ${i / BATCH_SIZE + 1}`)
  }
  
  console.log('Import complete!')
}

importContent()

Key Points

  • Use getCliClient() from sanity/cli - it's configured for Node.js
  • Run with --with-user-token flag for authenticated operations
  • Use createOrReplace() for idempotent imports (can run multiple times safely)
  • Batch operations into transactions to reduce API calls and avoid rate limits
  • Don't import browser-dependent code (like full Studio schemas with custom components)

More Resources

The demo content creation guide I linked above has extensive examples including:

  • Uploading images during imports
  • Converting HTML to Portable Text
  • Creating references between documents
  • Handling asynchronous operations

For more complex migrations with validation and better tooling, consider using sanity migration instead, which provides built-in batching, dry-run capabilities, and schema validation. The migration tooling is often preferred for content migrations as it provides additional conveniences and safety features.

Show original thread
9 replies
Here's an example of a script that manipulates content that uses
sanity exec
. What source are you trying to migrate from? Can you share the code you've tried?
I’m trying to pull in WordPress content using WPGraphQL and then import it using block-tools
The import piece isn’t there because it keeps failing just on importing the schema
Got it! I think I might know what the issue is. What does your
schemaData
look like?
Sidenote: if I'm wrong,
this Wordpress to Sanity migration repo will come in handy for you.
I added my
schema.js
to the Gist
Thanks. You don't need to concat all of your schema types here . the
.compile()
method takes an object that contains a block content array. So, your
schemaData
can be as simple as this:
const schemaData = {
  name: 'myBlog',
  types: [
    {
      type: 'object',
      name: 'blogPost',
      fields: [
        {
          title: 'Title',
          type: 'string',
          name: 'title',
        },
        {
          title: 'Body',
          name: 'body',
          type: 'array',
          of: [{ type: 'block' }],
        },
      ],
    },
  ],
}
You should be able to pass that directly to your
Schema.compile
.
Yep, that did it!
But if I want to try to parse WordPress blocks into Sanity block types, do I need to copy those into this other schema? (For instance, I want to convert
<figure class="wp-block-gallery">
into a
gallery
object)
Dope! You don't need to specify that schema during the migration but you will need to create a rule for the deserialization of those other types. For example, this would handle code and images for that default schema:
export const blockContentType = defaultSchema
  .get('blogPost')
  .fields.find(field => field.name === 'body').type;

export const generateBlocks = field =>
  blockTools.htmlToBlocks(field, blockContentType, {
    rules: [
      // Special rule for code blocks
      {
        deserialize(el, next, block) {
          if (el.tagName.toLowerCase() != 'pre') {
            return undefined;
          }
          const code = el.children[0];
          const childNodes =
            code && code.tagName.toLowerCase() === 'code'
              ? code.childNodes
              : el.childNodes;
          let text = '';
          childNodes.forEach(node => {
            text += node.textContent;
          });
          // Return this as an own block (via block helper function), instead of appending it to a default block's children
          return block({
            _type: 'code',
            language: 'javascript',
            text: text,
          });
        },
      },
      {
        deserialize (el, next, block) {
          if (el.tagName === 'IMG') {
            return block({
                children: [],
                _sanityAsset: `image@${el
                  .getAttribute('src')
                  .replace(/^\/\//, 'https://')}`
            })
          }

          if (
            el.tagName.toLowerCase() === 'p' &&
            el.childNodes.length === 1 &&
            el.childNodes.tagName &&
            el.childNodes[0].tagName.toLowerCase() === 'img'
          ) {
            return block({
                _sanityAsset: `image@${el.childNodes[0]
                  .getAttribute('src')
                  .replace(/^\/\//, 'https://')}`
            })
          }
          // Only convert block-level images, for now
          return undefined
        }
      }
    ],
  });
You'll need that other schema you created in the Studio, though! You can just add it to the Studio's schema folder.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?