Type something to search...
Debugging GROQ Queries With the Vision Plugin

Debugging GROQ Queries With the Vision Plugin

The Vision plugin is Sanity's built-in GROQ playground. It adds a Vision tab to Sanity Studio where you can run any query against your datasets, pass parameters, choose the API version and perspective, and inspect the JSON result. To use it, install @sanity/vision (new projects already include it), add visionTool() to the plugins array in sanity.config.ts, then open the Vision tab, write a query and press Fetch or Ctrl+Enter.

Most GROQ bugs come down to a filter that matches nothing, a reference that resolves to null or a parameter that isn't what you think it is. Debugging those in your front end means editing code, reloading and logging. Vision cuts that loop down to seconds. This guide covers installing and configuring the plugin, a tour of the interface, how to use parameters and perspectives, a step-by-step workflow for finding the cause of a broken query, and how to move a tested query into your code.

Installing Vision

Projects created with npm create sanity@latest already include Vision. To add it to an existing Studio, install the package at the same version as your sanity dependency:

npm install @sanity/vision

Then add it to your config alongside the Structure tool:

// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemaTypes'

export default defineConfig({
  name: 'default',
  title: 'My Site',
  projectId: 'your-project-id',
  dataset: 'production',
  plugins: [structureTool(), visionTool()],
  schema: { types: schemaTypes },
})

Restart the dev server with npx sanity dev and a Vision tab appears in the Studio's top navigation. If you're not sure where this file lives, see Sanity project structure explained.

Configuration Options

visionTool() accepts an options object:

OptionWhat it doesDefault
defaultApiVersionAPI version used for queries unless you pick anotherThe current date
defaultDatasetDataset selected when Vision opensThe Studio's dataset
datasetsLimits the dataset dropdown, as an array or a filter functionAll datasets
nameThe tool's URL segmentvision
titleLabel in the navigation barVision
iconNavigation iconAn eye icon

Setting defaultApiVersion to the same date your front end uses is the most useful option, because it means Vision behaves exactly like your production client:

visionTool({
  defaultApiVersion: '2025-02-19',
  defaultDataset: 'production',
  datasets: ['production', 'staging'],
})

Only in Development

Vision is safe to leave in a deployed Studio, since it only reads data the logged-in user can already access. Some teams prefer to keep it out of the editor-facing Studio anyway. Import isDev from sanity to load it only locally:

// sanity.config.ts
import { defineConfig, isDev } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'

export default defineConfig({
  // ...
  plugins: isDev ? [structureTool(), visionTool()] : [structureTool()],
})

If you deploy your Studio, the guide to deploying Sanity Studio explains how this affects the hosted version.

A Tour of the Interface

Vision has a small number of controls, and each one matters when debugging:

  • Dataset dropdown: chooses which dataset to query. A query that works in staging but not production often just means the content is different.
  • API version dropdown: GROQ behaviour can differ between API versions, so match your front end's apiVersion.
  • Perspective dropdown: chooses whether you see published content, drafts or raw documents.
  • Query editor: where you write GROQ, with syntax highlighting.
  • Params editor: a JSON object of parameter values for your query.
  • Fetch and Listen buttons: Fetch runs the query once. Listen opens a real-time listener so you can watch results change as editors work.
  • Result panel: the JSON response, which you can expand and collapse.
  • Query URL: the exact HTTP request Vision made to the Content Lake.

Vision is read-only. You can't run mutations from it, which makes it safe to experiment.

Using Parameters

Parameters in Vision work exactly as they do in code. Use $name in the query and supply the values in the params panel without the $:

*[_type == "post" && slug.current == $slug][0] {
  title,
  "author": author->name
}
{
  "slug": "hello-groq"
}

Test with the same parameters your front end will send, including edge cases: a slug that doesn't exist, an empty string, a number passed as a string. Parameter values must be valid JSON, so keys and strings need double quotes. If the params panel has a syntax error, Vision won't run the query. More on safe parameter usage in using GROQ parameters safely.

Choosing a Perspective

Perspectives decide which version of each document you see:

PerspectiveWhat you getUse it to debug
PublishedOnly published versionsWhat visitors see on the live site
DraftsDrafts where they exist, otherwise publishedWhat a preview or the Studio shows
RawEvery document, including drafts. IDs and release versionsDuplicates, missing drafts, ID issues

A very common source of confusion: an editor says "I changed the title" and your front end shows the old one. Switch Vision to the published perspective and you'll likely see the old title too, because the change is still a draft. The Content Lake explainer covers how drafts and published documents are stored.

The Query URL

After a query runs, Vision shows the full API URL it called, with the query, parameters and perspective encoded in it. It's handy for three things:

  • Comparing with your app: log the URL your client requests and diff it against Vision's to spot differences in API version, dataset or perspective.
  • Sharing: paste the URL into an issue so a colleague can see exactly what you ran.
  • Testing outside the Studio: for a public dataset, you can open it in a browser or call it with curl.
curl "https://your-project-id.api.sanity.io/v2025-02-19/data/query/production?query=count(*%5B_type%20%3D%3D%20%22post%22%5D)"
{"query":"count(*[_type == \"post\"])","result":42,"ms":4}

Unauthenticated requests never return drafts, so results from the URL can differ from Vision's when you're using a perspective that includes them. Private datasets need a token.

A Workflow for Debugging Queries

When a query returns the wrong result, work through it in small steps rather than staring at the whole thing.

1. Copy the Exact Query and Parameters

Paste the query from your code into Vision unchanged, and enter the exact parameter values your app sends. Set the API version and perspective to match your client config.

2. Strip It Back to the Filter

Remove the projection, slice and order() so you're left with the filter:

*[_type == "post" && slug.current == $slug && publishedAt <= now()]

If this returns nothing, the problem is in the filter. If it returns documents, the problem is further along.

3. Remove Conditions One at a Time

Delete conditions until results appear. The last one you removed is the culprit. Common findings:

  • Wrong field path: slug == $slug instead of slug.current == $slug.
  • Type mismatch: comparing a number field with a string parameter.
  • Single quotes: GROQ strings need double quotes.
  • Null values: a field that's missing on some documents, so the comparison returns null.

4. Inspect the Raw Document

When you're unsure what a field contains, look at one document in full:

*[_type == "post"][0]

This shows the real field names and structure, which may differ from what you expected, especially after schema changes. Fields renamed in the schema keep their old name in existing documents until they're migrated.

5. Add the Projection Back Piece by Piece

Restore the projection a few fields at a time. If a field comes back null, check it in isolation:

*[_type == "post" && slug.current == $slug][0] {
  author,
  "resolved": author->,
  "name": author->name
}

Seeing the raw author value next to the resolved document tells you immediately whether the reference is missing, points at a deleted or unpublished document, or is fine and the field name is wrong. The guide to dereferencing in GROQ covers the usual causes.

6. Check the Sort and Slice

Restore order() and the slice last. If the wrong items appear, check that order() comes before the slice, and that the sort has a tiebreaker such as _id if values can repeat.

7. Compare Timings

Vision reports how long each query takes. If a query is slow, try alternatives side by side, such as references($id) instead of author->_id == $id in a filter, or a tighter _type filter. Small changes can make a noticeable difference on large datasets.

Exploring Your Content

Vision isn't only for fixing bugs. A few queries worth saving for everyday use:

// How many documents of each type?
{
  "posts": count(*[_type == "post"]),
  "authors": count(*[_type == "author"]),
  "categories": count(*[_type == "category"])
}

// Posts missing required SEO data
*[_type == "post" && !defined(seo.description)] { _id, title }

// Documents nobody references (possible orphans)
*[_type == "category" && count(*[references(^._id)]) == 0] { _id, title }

// Recently edited documents
*[!string::startsWith(_type, "system.")] | order(_updatedAt desc) [0...10] { _type, _id, _updatedAt }

These are quick editorial audits you can run without writing a script. The GROQ cheat sheet has more patterns to try.

Moving a Query From Vision to Your Code

Once a query works in Vision, copy it into your code with defineQuery and pass the same parameters:

// src/sanity/queries.ts
import { defineQuery } from 'next-sanity'

export const POST_QUERY = defineQuery(`
  *[_type == "post" && slug.current == $slug][0] {
    title,
    "slug": slug.current,
    "author": author->name
  }
`)
// src/sanity/client.ts
import { createClient } from 'next-sanity'

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: '2025-02-19', // same as Vision
  useCdn: true,
})

Then run npx sanity typegen generate to get types for the result. Keep the API version, dataset and perspective in your client identical to what you tested in Vision, so there are no surprises.

One difference to remember: useCdn: true serves cached responses, so a change you see instantly in Vision may take a short time to appear in your app. If you need fresh data, such as in previews, use useCdn: false.


FAQ: The Vision Plugin

Vision is an official Sanity Studio tool for running GROQ queries. It adds a tab to the Studio where you can query any dataset, set parameters, choose an API version and perspective, and view the JSON result.

Install @sanity/vision if it isn't already in your project, import visionTool from it in sanity.config.ts, and add visionTool() to the plugins array. Restart the Studio and a Vision tab appears in the navigation.

No. Vision is for queries and listeners only. To create, update or delete documents, use the Sanity CLI, a migration script or the Mutation API through @sanity/client.

Usually because of a different API version, dataset or perspective, or because your site uses the CDN and is serving a cached response. Match all three settings to your client config and compare the query URLs.

Write the parameter in the query with a dollar sign, such as $slug, and add a JSON object in the params panel with the key without the dollar sign, such as slug with a string value. The params must be valid JSON.

It's not required, because Vision only shows data that the logged-in user can already read. If you'd rather keep the editor interface simple, load it only in development by checking isDev from the sanity package in your config.


Conclusion

The Vision plugin is the fastest way to write and debug GROQ. Add visionTool() to your config, set its default API version to match your front end, and use the dataset, perspective and params controls to reproduce exactly what your app requests.

When a query misbehaves, strip it back to the filter, remove conditions one by one, inspect the raw documents and rebuild the projection piece by piece. Once it works, copy it into defineQuery, keep your client settings in line with what you tested, and move on with confidence.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper