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:
- Custom GROQ functions are available on all API versions except v1.
Function anatomy
Custom functions look similar to other GROQ functions, but with some limitations. They include a namespace and accept a 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:
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.
Examples
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" },
"belongings": [
{"name": "laptop"},
{"name": "badge"},
{"name": "backpack"}
],
pet: [
{"_ref": "dog"},
{ "_ref": "dog2" }
]
}{
"_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": {
"age": 99,
"name": {
"first": "Jane",
"last": "Doe"
}
}
}]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"}
]
}]PTE blocks
Reusing the logic for parsing PTE 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
},
// etc
}
}
}
};
*[_type == "article"] {
_id,
title,
"slug": slug.current,
"content": ex::blocks(content[]),
"description": ex::blocks(description[])
}Limitations
At this time, functions are limited to the formats displayed above. Additionally, custom functions do not yet support:
- Recursion.
- Accessing the parent scope.
- Passing multiple parameters.
- Accessing the function parameter more than once in the function body.
Query Cheat Sheet - GROQ
Data query examples.
How Queries Work – GROQ
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.
TypeGen integration
Since December 2025, TypeGen supports custom GROQ functions. 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.