> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# 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 set `apiVersion` explicitly on your client.
- For generated TypeScript types, `groq-js` 1.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.

**Query**

```groq
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:

- `ex` is the namespace.
- `name` is the function name.
- `$author` is 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](https://www.sanity.io/docs/http-reference/query), or [the Vision Tool](https://www.sanity.io/docs/content-lake/the-vision-plugin). For example, in `@sanity/client`:

**lib/posts.ts**

```typescript
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](https://www.sanity.io/docs/specifications/groq-functions) 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.

**Person**

```json
{
  "_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" }
  ]
}
```

**Occupation**

```json
{
  "_id": "developer",
  "_type": "occupation",
  "title": "Software Engineer"
}
```

**Pet 1**

```json
{
  "_id": "dog",
  "_type": "pet",
  "name": "Pookie"
}
```

**Pet 2**

```json
{
  "_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.

**Query**

```groq
fn ex::details($person) = $person{name, age}; 
*[_type == "person"] { "info": ex::details(@) }

```

**Response**

```json
[{
  "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.

**Query**

```groq
fn ex::title($ref) = $ref->{title};
*[_type == "person"] { "occupation": ex::title(occupation) }
```

**Response**

```json
[{
  "occupation": {
    "title": "Software Engineer"
  }
}]
```

### Array projection

This function iterates through the person's `belongings` to display their names.

**Query**

```groq
fn ex::items($arr) = $arr[]{name}; 
*[_type == "person"] { "stuff": ex::items(belongings) }
```

**Response**

```json
[{
  "stuff": [
    {"name": "laptop"},
    {"name": "badge"},
    {"name": "backpack"}
  ]
}]
```

### Array of references

This function follows each reference in the person's `pet` key.

**Query**

```groq
fn ex::pets($items) = $items[]->{name};
*[_type == "person"] {"pet": ex::pets(pet)}

```

**Response**

```json
[{
  "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.

**Query**

```groq
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:

**lib/queries.ts**

```typescript
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 shape
```

## Reusing 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:

**lib/queries.ts**

```typescript
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) }`)
```

> [!NOTE]
> 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.

#### Related articles

[GROQ query cheat sheet](https://www.sanity.io/docs/content-lake/query-cheat-sheet)
Data query examples.

[How GROQ queries work](https://www.sanity.io/docs/content-lake/how-queries-work)
A tutorial on using the Sanity query language GROQ.

[GROQ feature support across Sanity](https://www.sanity.io/docs/content-lake/groq-feature-support-by-context)
A summary of GROQ language support and limitations across different Sanity contexts.

[Paginating with GROQ](https://www.sanity.io/docs/developer-guides/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.



## Related changelog entries

Entries are listed newest first; each link points to the full entry as markdown. Follow one when you need to know what changed, when, or why — for example, to summarize recent updates, explain behavior that differs from older documentation, or check whether a fix has shipped.

- [Sanity Studio v5.0.0: React 19.2 as baseline, TypeGen improvements](https://www.sanity.io/docs/changelog/fd3ab62e-9264-4e7b-825a-fd4f99abd481.md) — December 15, 2025