How to add multiple custom document actions in Sanity plugins

13 replies
Last updated: Apr 12, 2021
Hello, I am about to create a second plugin to add a document action. How can I make sure the second plugin does not overwrite the first plugin?
I used this documentation to create the first one:
https://www.sanity.io/docs/document-actions
It seems there is no way to reference the previously implemented
sanity/base/document-actions/resolver
part. I might have missed some documentation, so any pointers would be helpful.
AI Update

In Sanity Studio v3, you don't need to worry about plugins overwriting each other's document actions - the system handles composition automatically! When you add multiple plugins or define multiple actions, they all get combined into the actions menu.

Here's how it works:

Adding Multiple Actions

When you register document actions in your sanity.config.ts, you can provide either a static array or a resolver function. If you want to add actions from multiple sources (your own custom actions plus the defaults), use the resolver function approach:

import {defineConfig} from 'sanity'
import {FirstCustomAction} from './first-action'
import {SecondCustomAction} from './second-action'

export default defineConfig({
  // ... rest of config
  document: {
    actions: (prev, context) => {
      // prev contains all previously registered actions (from plugins and defaults)
      // Just add your new actions to the array
      return [FirstCustomAction, SecondCustomAction, ...prev]
    },
  },
})

The prev parameter contains all actions that have been registered before yours - including actions from plugins and the Studio defaults (publish, duplicate, delete, etc.). By spreading ...prev into your returned array, you preserve all existing actions while adding your new ones.

Multiple Plugins with Actions

If you're creating separate plugins that each add document actions, they compose automatically:

// first-plugin.ts
import {definePlugin} from 'sanity'

export const firstPlugin = definePlugin({
  name: 'first-plugin',
  document: {
    actions: (prev) => [MyFirstAction, ...prev]
  }
})

// second-plugin.ts  
export const secondPlugin = definePlugin({
  name: 'second-plugin',
  document: {
    actions: (prev) => [MySecondAction, ...prev]
  }
})

// sanity.config.ts
export default defineConfig({
  plugins: [
    firstPlugin(),
    secondPlugin(), // These compose automatically!
  ]
})

Each plugin receives the actions from previous plugins through the prev parameter, so they naturally compose without overwriting each other.

Key Differences from Studio v2

This is much simpler than Studio v2's parts system (which used part:@sanity/base/document-actions/resolver). In v2, you had to manually import the default resolver using the parts system:

// Studio v2 approach (deprecated)
import defaultResolve from "part:@sanity/base/document-actions"

export default function resolveDocumentActions(props) {
  return [...defaultResolve(props), MyCustomAction]
}

Studio v3 eliminates this complexity - you just work with the prev parameter in your resolver function, and the framework handles all the composition for you.

Check out the document actions documentation for more examples, including how to conditionally show actions, extend built-in actions, and create actions with custom dialogs.

Hey there! The resolver you created will work with m
Hey there! The
resolver
you previously created will work with multiple custom document actions.
The document action resolver provides you with a central place to make decisions about the collection of actions enabled for a given document state. 
To add another action, you would need to import the action into your
resolver
file, then add it to the array you’re returning in your
resolveDocumentActions
function.
Hmm, maybe I didn't explain good enough (or simple don't understand).
I envision two isolated plugins that are not aware of each other. Both want to add a custom document action. The method described in the documentation however assumes there is only one implementation of
sanity/base/document-actions/resolver
. Is there a way to obtain a reference to other implementations of
sanity/base/document-actions/resolver
so I don't shadow the implementation of another plugin?
Or maybe I should go about this another way.
I believe you may not be understanding. You do not need to create multiple resolvers. You use a single resolver for all of the custom actions you’ve created. The different actions you’ve created would not need to be aware of each other. They just need to be added to the array your resolver returns.
I still don't understand (sorry).
Plugin 1 implements
sanity/base/document-actions/resolver
and adds an action. Plugin 2 also implements
sanity/base/document-actions/resolver
and also adds an action. Will sanity use all implementations of
sanity/base/document-actions/resolver
and compare the arrays?
Ok, let’s see if I can help clear this up. Can you show me the parts in your sanity.json that refer to those two plugins?
Ok, let’s see if I can help clear this up. Can you show me the parts in your sanity.json that refer to those two plugins?
As well as your resolve document actions file, come to think of it.
I haven't written it yet. But it would be like this:
plugin 1

"parts": [ 
  ... 
  { 
    "implements": "part:@sanity/base/document-actions/resolver", 
    "path": "resolveDocumentActions.js" 
  } 
]
plugin 2

"parts": [ 
  ... 
  { 
    "implements": "part:@sanity/base/document-actions/resolver", 
    "path": "resolveDocumentActions.js" 
  } 
]
project
sanity.json

"plugins": [
  ...
  "plugin1",
  "plugin2"
],

OK, so your
resolveDocumentActions.js
for both of those plugins would be the same file. It would look something like this:
import defaultResolve from 'part:@sanity/base/document-actions'

import {pluginOneAction} from './path/to/pluginOneAction'
import {pluginTwoAction} from './path/to/pluginTwoAction'

export default function resolveDocumentActions(props) {
  return [defaultResolve, pluginOneAction, pluginTwoAction]
}
OK, so your
resolveDocumentActions.js
for both of those plugins would be the same file. It would look something like this:
import defaultResolve from 'part:@sanity/base/document-actions'

import {pluginOneAction} from './path/to/pluginOneAction'
import {pluginTwoAction} from './path/to/pluginTwoAction'

export default function resolveDocumentActions(props) {
  return [defaultResolve, pluginOneAction, pluginTwoAction]
}
Also note, if you only want to show an action under certain conditions, you could conditionally show them using this .
I was afraid this would be the case. So in every project where I would like to use plugins that provide document actions I need to implement a resolver... I hoped there would be a built-in way to prevent that.
I guess my best option is to write a custom document actions plugin that does something like this:


"parts": [ 
  ... 
  { 
    "implements": "part:@sanity/base/document-actions/resolver", 
    "path": "resolveDocumentActions.js" 
  },
  {
    "name": "part:@kaliber/document-actions/resolver",
    "description": "..."
  }
]

// resolveDocumentActions.js
import defaultResolve from 'part:@sanity/base/document-actions'
import resolvers from 'all:@kaliber/document-actions/resolver'

export default function resolveDocumentActions(props) {
  return resolvers.reduce(
    (result, resolver) => resolver(props, result),
    defaultResolve(props)
  )
}
This is just an example of how it could be solved. I'm not sure what a smart API would look like since I did not have time to think about it yet. That was why I was hoping there would be another way to access other implementations of the same part. Maybe you could check if this pattern has been encountered before and has a 'Sanity preferred' way of solving.

Anyway, thank you for your time!

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?