Auto-sync artwork references between Artist and Exhibition documents

8 replies
Last updated: Apr 9, 2020
Client insisting on an unwieldy content setup –You have two document types,
Exhibition
and
Artist
– once they’ve made an
Artwork
, in which they define a reference to a specific
Artist
, they would also like this to appear in an array of references in the
Artist
document. I’m wondering if it’s possible to:
A) automatically push this reference, as described above
or...
B) the references to `artwork`s have to be setup in each Exhibition document anyway (you might only show 5 artworks from an artist’s entire catalogue). Is it possible to import references an array of document references from one document type –
exhibition
– to another automatically?
Not essential, they can do manually – but it seems like this might be in reach?
AI Update

This is definitely achievable! You have a couple of solid approaches. For automatic syncing, I'd recommend using Sanity Functions, which are serverless functions that run on Sanity's infrastructure and respond to document changes.

A) Automatically pushing references when an Artwork is created

Sanity Functions let you execute custom logic whenever changes occur in your content without requiring your own infrastructure. When an Artwork is created or updated with an Artist reference, a function can automatically update that Artist document to include the reference back.

Here's how to set this up. First, initialize Blueprints in your project (requires Sanity CLI v3.92.0+, v4.5.0+ recommended):

npx sanity blueprints init
npx sanity blueprints add function --name sync-artwork-to-artist

This creates a sanity.blueprints.ts file and a function directory. Here's what your configuration might look like:

// sanity.blueprints.ts
import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'

export default defineBlueprint({
  resources: [
    defineDocumentFunction({
      type: 'sanity.function.document',
      name: 'sync-artwork-to-artist',
      src: './functions/sync-artwork-to-artist',
      event: {
        on: ['create', 'update'],
        filter: '_type == "artwork" && defined(artist._ref)',
      },
    }),
  ],
})

Then create your function handler:

// functions/sync-artwork-to-artist/index.ts
export default async function(event, context) {
  const { getClient } = context
  const client = getClient({ apiVersion: '2024-01-01' })
  const artwork = event.document
  
  if (artwork.artist?._ref) {
    // Check if this artwork reference already exists to avoid unnecessary updates
    const artist = await client.getDocument(artwork.artist._ref)
    const existingRefs = artist.artworks || []
    const alreadyExists = existingRefs.some(ref => ref._ref === artwork._id)
    
    if (!alreadyExists) {
      await client
        .patch(artwork.artist._ref)
        .setIfMissing({ artworks: [] })
        .append('artworks', [{ _type: 'reference', _ref: artwork._id }])
        .commit()
    }
  }
}

Deploy with:

npx sanity blueprints deploy

B) Importing references from Exhibition to Artist

Similarly, you could create a function that watches for changes to Exhibition documents and updates the related Artist records when artworks are added to exhibitions. The pattern would be the same—listen for exhibition updates, extract the artwork references, look up which artists created those works, and update the artist documents accordingly.

Alternative: Query-based approach (simpler, no automation needed)

Before going the automation route, seriously consider whether you actually need to store these bidirectional references. You can query for reverse references using GROQ without storing them:

// Get all artworks by this artist
*[_type == "artwork" && artist._ref == $artistId]

// Get all exhibitions featuring this artist's work
*[_type == "exhibition" && references(*[_type == "artwork" && artist._ref == $artistId]._id)]

This approach keeps your data normalized and avoids the complexity of keeping bidirectional references in sync. You only store references in one direction and query for the reverse relationship when needed. It's simpler, less prone to data inconsistencies, and easier to maintain.

The Functions approach makes sense if you need extremely fast reads and can accept slightly slower writes, or if you need to trigger additional side effects when these relationships change. The query approach is simpler and keeps your data model cleaner.

Important considerations

Experimental feature: Blueprints are currently experimental, so the APIs may change.

Avoid recursive loops: Functions don't currently prevent recursive loops, so be careful when writing functions that modify documents. In the example above, I added a check to only update the artist if the artwork reference isn't already present—this prevents the function from repeatedly triggering itself.

Testing: You can test your functions locally before deploying:

npx sanity functions test sync-artwork-to-artist --dataset production

The bidirectional reference syncing is definitely achievable with Functions, but I'd encourage you to start with the query-based approach unless you have specific performance requirements that demand pre-computed relationships.

Show original thread
8 replies
NB: Why is this necessary? Why not show all artworks for a given artist on their page, and order by a given property – e.g.
artwork.date
? Unfortunately they want very fine-grained control over which artworks are shown, and in what order. So an array of references absolutely seems like the best route, despite all the extra work involved. They will only have around 20 artists at any one time, with maybe max 60 artworks for each – so its a little crazy but not totally insane (I hope?).
Have you taken a look at custom workflows? https://www.sanity.io/docs/custom-workflows You can have a document action on an
artwork
document called something like “Add to exhibition”, that handles adding it to an exhibition
Oh thanks so much
user H
– no I’d must’ve missed this being added, sorry! Looks very useful
😃 It’s super handy! It was only recently released, so maybe that’s why 👍
It’s brilliant, thank you
user H
! All changing so fast. I’m nearly there! I’ve got a button which publishes an
artwork
, and in doing so inserts a reference in an array of references to
artworks
on an artist document.
How can I check if that item already exists in the array? This is the final piece of the puzzle... I need to wrap this is an if statement, I just don’t know how to check if the item exists in the array on the document I want to insert it in?

      patch.execute([{
        "insert": {
          "before": "artworks[0]",
          "items": [{"_ref": <http://props.id|props.id>, "_type":"reference", "_key": <http://props.id|props.id> }]
        }
      }])
Here’s my whole code:


import { useState, useEffect } from 'react'
import { useDocumentOperation } from '@sanity/react-hooks'

export function setAndPublishAction(props) {

  if (props.type !== 'artwork') {
    return null
  }

  const { publish } = useDocumentOperation(<http://props.id|props.id>, props.type)
  const { patch } = useDocumentOperation(props.published.artist._ref, 'artist')
  const [isPublishing, setIsPublishing] = useState(false)

  useEffect(() => {
    // if the isPublishing state was set to true and the draft has changed
    // to become `null` the document has been published
    if (isPublishing && !props.draft) {
      setIsPublishing(false)
    }
  }, [props.draft])

  return {
    disabled: publish.disabled,
    label: isPublishing ? 'Publishing…' : 'Amy, please don't press this one',
    onHandle: () => {
      // This will update the button text
      setIsPublishing(true)
      // Add artwork to "artworks" array on referenced Artist document
      patch.execute([{
        "insert": {
          "before": "artworks[0]",
          "items": [{"_ref": <http://props.id|props.id>, "_type":"reference", "_key": <http://props.id|props.id> }]
        }
      }])
      // Perform the publish
      publish.execute()
      // Signal that the action is completed
      props.onComplete()
    }
  }
}
Could you fetch all the artworks using the client and check it that way?
user H
thank you once again ! I thought I’d had a bit of a brainwave this morning – could possibly just check if the document has been published before...
if (props.published) { return null }
but this ended up being trickier than I expected;
I ended up doing what you suggested and trying a client fetch / patch within the studio (never done before!) and it worked great – so thank you


import CheckIcon from 'part:@sanity/base/check-icon'
import { useState, useEffect } from 'react'
import { useDocumentOperation } from '@sanity/react-hooks'
import sanityClient from '@sanity/client'
const client = sanityClient({
  projectId: 'id',
  dataset: 'production',
  token: 'token', // or leave blank to be anonymous user
  useCdn: false
})
import { nanoid } from 'nanoid'

export function SetAndPublishAction(props) {

  const { publish } = useDocumentOperation(<http://props.id|props.id>, props.type)
  const [isPublishing, setIsPublishing] = useState(false)

  useEffect(() => {
    // if the isPublishing state was set to true and the draft has changed
    // to become `null` the document has been published
    if (isPublishing && !props.draft) {
      setIsPublishing(false)
    }
  }, [props.draft])

  return {
    disabled: publish.disabled,
    icon: CheckIcon,
    label: isPublishing ? 'Publishing…' : 'Publish',
    onHandle: () => {
      // This will update the button text
      setIsPublishing(true)
      // Add artwork to "artworks" array on referenced Artist document
      if (props.type == 'artwork') {
        if (props.draft.artist._ref) {
          const artistId = props.draft.artist._ref
          const query = '*[_type == "artist" && _id == $artistId][0].artworks'
          const params = { artistId }
          client.fetch(query, params).then(artworks => {
            const check = artworks.some(artwork => artwork._ref === <http://props.id|props.id>)
            if (check) {
              console.log('Artwork already referenced on Artist');
            } else {
              console.log('Artwork now added to Artist');
              client
                .patch(artistId)
                // Ensure that the `artworks` arrays exists before attempting to add items to it
                .setIfMissing({artworks: []})
                // Add the items after the last item in the array (append)
                .insert('before', 'artworks[0]', [
                  // Add a `_key` unique within the array to ensure it can be addressed uniquely
                  // in a real-time collaboration context
                  {_key: nanoid(), _ref: <http://props.id|props.id>, _type:"reference" }
                ])
                .commit()
            }
          })
        } else {
          alert('Please provide an artist reference.')
        }
      }
      // Perform the publish
      publish.execute()
      // Signal that the action is completed
      props.onComplete()
    }
  }
}
Awesome, happy you got it to work 😃

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.

Was this answer helpful?