
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:
| Option | What it does | Default |
|---|---|---|
defaultApiVersion | API version used for queries unless you pick another | The current date |
defaultDataset | Dataset selected when Vision opens | The Studio's dataset |
datasets | Limits the dataset dropdown, as an array or a filter function | All datasets |
name | The tool's URL segment | vision |
title | Label in the navigation bar | Vision |
icon | Navigation icon | An 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
stagingbut notproductionoften 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:
| Perspective | What you get | Use it to debug |
|---|---|---|
| Published | Only published versions | What visitors see on the live site |
| Drafts | Drafts where they exist, otherwise published | What a preview or the Studio shows |
| Raw | Every document, including drafts. IDs and release versions | Duplicates, 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 == $sluginstead ofslug.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.


