🔮 Sanity Create is here. Writing is reinvented. Try now, no developer setup
Last updated October 05, 2021

GROQ-Powered Webhooks – Intro to Filters

By Martin Jacobsen & Knut Melvær

GROQ-powered webhooks give you precise control over the circumstances and conditions under which your webhooks will trigger. In this article, we'll take a closer look at the possibilities offered by GROQ filters.

Triggering webhooks with precision

The two GROQ super-powers webhooks have are filters and projections. The latter enables you to construct your request's payload to your exact needs and is discussed in an article of its own. In this article, we'll be discussing the former. Filters let you define the circumstances and conditions under which your webhook should trigger with immaculate precision.

If you want to follow along, go ahead and set up a new webhook in the project management console. You can use a service like webhook.site, or Beeceptor to debug and test your webhook.


This article assumes you are comfortable with the basic concept of webhooks, and how to create one in your Sanity project. If you're not quite up to speed, have a look at the webhook docs and come back when you're ready to do some filtering!

Event types

Before dedicating our attention exclusively to GROQ-filters, let's briefly look at the set of checkboxes labeled "Trigger on", found immediately before the filter input field.

Webhooks can be triggered when a document is created, updated, deleted, or any combination of these.

  • Create - triggers on the creation of a new document.
  • Update - triggers on every change to a document once created.
  • Delete - triggers on the deletion of a document

Between these, you'll be able to react to all major interactions with the documents relevant to you – the selection of which we'll be spending the rest of this article getting increasingly particular about.


By default, your webhooks will not trigger on draft-events. I.e. They will only trigger when changes to the document are published and not for every single occurrence while you edit. Triggering on draft-events can be enabled, but be careful or you may end up causing huge amounts of traffic to your endpoint!

Filters in GROQ

This is where, using GROQ, you define the criteria by which a document event should trigger your webhook. If you are already familiar with GROQ, the filter is the bit that you'll often see inside the square brackets at the start of your query, often preceded by an asterisk.

*[ /*Filters go here!*/ ]

A typical case of using a filter in a query might look something like this:

// Returns every document with a type of post 
*[_type == "post"]

When creating filters for our webhooks we skip the asterisk and square brackets and just type the filtering conditions right into the input field. Translating the scenario from above, our webhook filter would look like this:

GROQ Fundamentals

This minimal example is already a quite powerful feature – enabling you to react to changes to specific types of content – but let's get a bit more creative! Let's expand our filter to check for the value of a boolean field named featured in the document, and only trigger if it's set to true, and just to make it interesting let's add another document type called article as well.

// Trigger on events for posts and articles with featured set to true
_type in ["post", "article"] && featured == true

Just as in normal GROQ-queries you can use logical operators to filter on a range of different field types, and you can combine any number of these in order to get exactly the result you want.

// Trigger on events for movies that are quite popular and relatively recent
_type == "movie" && popularity > 15 && releaseDate > "2016-01-01"

// Trigger on events for articles tagged with a carefree attitude
_type == "article" && "yolo" in tags

// Trigger on events for posts about extraterrestrials 
_type == "post" && body[].children[].text match "alien"

Following references to filter on content in the referenced document also works as you'd expect:

// Trigger on events for posts by a specific author
_type == "post" && author->name == "Sinjoro Ajnulo"

Using Functions

You can also use GROQ functions, such as dateTime(), references(), or defined().

// Trigger on events for posts published after 2016-01-01
_type == "post" && dateTime(_createdAt) > dateTime("2016-01-01T00:00:00Z")

// Trigger on events for books with a value set for the author field
_type == "book" && defined(author)

// Trigger on events for books referencing a specific author
_type == "book" && references("sinjoro-ajnulo")

Namespaced functions are also available, such as pt::text() which gives you a plain text version of any Portable Text rich text field, or the sanity::-prefixed functions which are helpful for querying about the environment from which the webhook was triggered.

// Trigger on events for posts that mention a certain term in the body
_type == "post" && pt::text(body) match "Sinjoro"

// Trigger if the projectId matches the expected value
sanity::projectId() == "<projectId>"

// Trigger if the change occurred in one of the specified datasets
sanity::dataset() in ["test", "staging"]

Querying "what changed?" with Delta-GROQ

Delta-GROQ is an extension to the GROQ language designed specifically to enable you to reason about changes made to a document. Currently, the following delta-functions are available:

  • before() – returns the matching documents as they were before the change.
  • after() – returns the matching documents after the change.
  • delta::changedAny() – returns true if specified field values have changed.
  • delta::changedOnly() – which returns true if only specified field values have changed.

The aptly named functions before()and after() let you compare the document in its entirety before and after the change event was executed. You may also use dot notation to check the value of any single field. E.g. before().title == after().title.


While you can use projection on the results of before() and after() in other contexts – like this before(){ title, description } – this won't work in filters, as objects are not checked for deep equality and will always return true.

// Trigger if the price has gone down
_type == "product" && before().price > after().price

// Trigger if title changes to no longer including the string 'DRAFT'
before().title match "DRAFT*" && !(after().title match "DRAFT*")

The namespaced functions delta::changedAny() and delta::changedOnly() are helpful when you want to run the webhook only when certain fields have been updated, or indeed if only certain fields have been updated.

// Trigger if one or more of the specified fields have been updated
_type == "product" && delta::changedAny((name, price, stock))

// Do not trigger if featured is the only field updated
_type == "product" && !delta::changedOnly(featured)


Notice the double parentheses when more than one field is used in delta::changedAny() or delta::changedOnly().

In conclusion

In this article we've demonstrated some more or less plausible examples of how using GROQ filters enables unmatched granularity in specifying the circumstances and conditions under which your webhooks should be triggered. While we've touched upon a number of different methods and techniques, we haven't even revealed the tip of the iceberg representing the capabilities of GROQ. For inspiration, check out the GROQ Query Cheat Sheet!

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

Other guides by authors