Querying reverse relationships in Sanity with GROQ
Yes! This is a perfect use case for GROQ's references() function, which lets you do reverse reference lookups. Since you have "work" documents that reference "work categories", you can query all categories and then find which works reference each one.
Here's how to do it with a GROQ query:
*[_type == "workCategory"] {
_id,
title,
"relatedWorks": *[_type == "work" && references(^._id)] {
_id,
title,
slug,
// any other fields you need from work
}
}The key part is references(^._id) - this checks if any "work" documents contain a reference to the current category's ID. The ^ operator accesses the parent scope (the category being iterated over).
In your Next.js frontend, you'd use this with the Sanity client:
import { client } from '@/sanity/lib/client'
const query = `*[_type == "workCategory"] {
_id,
title,
"relatedWorks": *[_type == "work" && references(^._id)] {
_id,
title,
slug
}
}`
export async function getWorkCategories() {
const categories = await client.fetch(query)
return categories
}Then you can loop over the categories in your component:
{categories.map((category) => (
<div key={category._id}>
<h2>{category.title}</h2>
<ul>
{category.relatedWorks.map((work) => (
<li key={work._id}>{work.title}</li>
))}
</ul>
</div>
))}The references() function documentation explains this pattern in detail, and there's also a helpful backlinks recipe that shows similar reverse reference patterns.
One performance note: if you have many works per category, you might want to add pagination or limits to the subquery to keep response sizes manageable.
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.