Custom GROQ functions
Learn how to create your own GROQ functions.
Sometimes you find yourself repeating the same portion of a GROQ query across multiple queries, or even within a single complex query. Custom functions for GROQ allow you to create modular, reusable sub-queries.
Prerequisites
- An API version other than
v1. Custom GROQ functions are available on every other API version, so setapiVersionexplicitly on your client. - For generated TypeScript types,
groq-js1.24.0 or later.
Function anatomy
Custom functions look similar to other GROQ functions, but with some limitations. They require a namespace and accept exactly one parameter. Let's look at an example function that follows a reference and returns a projection that combines an author's first and last name.
fn ex::name($author) = $author-> { "name": firstName + " " + lastName };
*[_type == "post"]{
"author": ex::name(author)
}All functions start with the fn keyword and contain a namespace, name, parameter, and function body. In the example above:
exis the namespace.nameis the function name.$authoris the parameter.$author-> { "name": firstName + " " + lastName }is the body.
Custom function declarations must happen at the start of the GROQ query and each declaration must end with a semicolon (;). You can use them anywhere you'd normally send a GROQ query, such as a Sanity client, the HTTP query API, or the Vision Tool. For example, in @sanity/client:
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2026-09-03',
useCdn: true,
})
const QUERY = `
fn ex::name($author) = $author-> { "name": firstName + " " + lastName };
*[_type == "post"]{
"author": ex::name(author)
}`
const posts = await client.fetch(QUERY)See the GROQ functions reference for additional details.
Supported function formats
Custom functions support a limited set of formats at this time:
$param{...}$param->{...}$param[]{...}$param[]->{...}
Let's use the following documents as an example to explore each format. There is a person document, an occupation document, and two pet documents.
{
"_id": "a",
"_type": "person",
"name": [
{
"first": "Jane",
"last": "Doe"
}
],
"age": 99,
"occupation": { "_ref": "developer", "_type": "reference" },
"belongings": [
{"name": "laptop"},
{"name": "badge"},
{"name": "backpack"}
],
"pet": [
{ "_ref": "dog", "_type": "reference" },
{ "_ref": "dog2", "_type": "reference" }
]
}{
"_id": "developer",
"_type": "occupation",
"title": "Software Engineer"
}{
"_id": "dog",
"_type": "pet",
"name": "Pookie"
}{
"_id": "dog2",
"_type": "pet",
"name": "Snookie"
}Basic projection
First we'll define a function that returns a basic projection. This function, ex::details, takes a $person parameter and returns a projection containing their name and age. To use the function, we pass in @ to represent the person returned by the filter.
fn ex::details($person) = $person{name, age};
*[_type == "person"] { "info": ex::details(@) }[{
"info": {
"name": [
{
"first": "Jane",
"last": "Doe"
}
],
"age": 99
}
}]Follow references
It's common to follow references to include part or all of their contents in the referencing object. This function follows the person's occupation reference and returns a projection with their title.
fn ex::title($ref) = $ref->{title};
*[_type == "person"] { "occupation": ex::title(occupation) }[{
"occupation": {
"title": "Software Engineer"
}
}]Array projection
This function iterates through the person's belongings to display their names.
fn ex::items($arr) = $arr[]{name};
*[_type == "person"] { "stuff": ex::items(belongings) }[{
"stuff": [
{"name": "laptop"},
{"name": "badge"},
{"name": "backpack"}
]
}]Array of references
This function follows each reference in the person's pet key.
fn ex::pets($items) = $items[]->{name};
*[_type == "person"] {"pet": ex::pets(pet)}[{
"pet": [
{"name": "Pookie"},
{"name": "Snookie"}
]
}]Reuse Portable Text projections
Reusing the logic for parsing Portable Text blocks is a common use case for functions. This example parses a set of blocks regardless of the incoming blocks.
fn ex::blocks($arr) = $arr {
...,
_type == "docsCallout" => {
...,
content[] {
...,
markDefs[] {
...,
_type == "link" => {
isInternal,
_key,
_type,
reference->,
url
},
_type == "acronym" => {
_key,
_type,
value
}
}
}
}
};
*[_type == "article"] {
_id,
title,
"slug": slug.current,
"content": ex::blocks(content[])
}Limitations
Function bodies are limited to the four formats listed under Supported function formats. Any other body is rejected with function body is not of type ast.Projection. Custom functions also do not yet support:
- Recursion:
cyclic dependency detected in function definition. - Accessing the parent scope:
parent usage is invalid. - Passing more or fewer than one parameter:
expected ')' following function arguments. - Accessing the function parameter more than once in the function body:
the function argument cannot be used more than once in the function body.
TypeGen integration
TypeGen supports custom GROQ functions from groq-js 1.24.0 or later, which ships with the Sanity CLI. Use defineQuery as usual, and TypeGen generates TypeScript types that account for your function's return shape:
import {defineQuery} from 'groq'
const query = defineQuery(`
fn ex::name($author) = $author-> { "name": firstName + " " + lastName };
*[_type == "post"]{ title, "author": ex::name(author) }
`)
// TypeGen generates types that include the function's return shapeReusing functions across queries
Custom functions must be declared at the start of each query. To reuse a function across multiple queries, define it as a string constant and prepend it:
import {defineQuery} from 'groq'
const authorNameFn = `fn ex::name($author) = $author-> { "name": firstName + " " + lastName };`
// Reuse the same function in different queries
const postsQuery = defineQuery(`
${authorNameFn}
*[_type == "post"]{ title, "author": ex::name(author) }`)
const articlesQuery = defineQuery(`
${authorNameFn}
*[_type == "article"]{ title, "author": ex::name(author) }`)There is no global function registry. Each query must include its own function declarations. The string interpolation pattern above keeps your function definitions in one place while allowing reuse.
GROQ query cheat sheet
Data query examples.
How GROQ queries work
A tutorial on using the Sanity query language GROQ.
GROQ feature support across Sanity
A summary of GROQ language support and limitations across different Sanity contexts.
Paginating with GROQ
Learn efficient pagination in GROQ using cursor-based filtering instead of array slicing. Covers tiebreakers for non-unique fields and batch processing.