
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeI completely understand your frustration - you're definitely not alone in finding the Document Actions API challenging to work with! The conceptual gap between "here's the API" and "here's how to actually extend default behavior safely" is real. Let me break down the key concepts that should help you wrap the default publish action successfully.
The key insight is that you want to wrap the default publish action, not recreate it. The problem is that the default publish action is a function component that returns an action object, and you need to preserve its entire implementation while adding your own logic.
Based on the useDocumentOperation hook documentation and patterns from the community, here's the conceptual approach:
// In your sanity.config.ts
import {defineConfig} from 'sanity'
import defaultResolve, {PublishAction} from 'sanity'
export default defineConfig({
// ... other config
document: {
actions: (prev, context) => {
// Replace the default PublishAction with your wrapped version
return prev.map((Action) =>
Action === PublishAction ? MyCustomPublishAction : Action
)
}
}
})The default publish action handles several critical things that you don't want to reimplement:
disabled property to prevent publishing when validation failsdrafts. prefix and the relationship between draft and published documentsWhen you see publish.disabled in action code, that's checking validation state. When you see publish.execute(), that's the actual operation being performed.
Here's a pattern that adds a field update at publish time while preserving all default behavior:
import {useState, useEffect} from 'react'
import {useDocumentOperation} from 'sanity'
export function SetPublishDateAction(props) {
const {patch, publish} = useDocumentOperation(props.id, props.type)
const [isPublishing, setIsPublishing] = useState(false)
useEffect(() => {
// Reset state when publish completes (draft becomes null)
if (isPublishing && !props.draft) {
setIsPublishing(false)
}
}, [props.draft, isPublishing])
return {
// CRITICAL: Preserve the disabled state from the default action
// This maintains validation behavior
disabled: publish.disabled,
label: isPublishing ? 'Publishing…' : 'Publish',
onHandle: async () => {
setIsPublishing(true)
// Your custom logic: update a field before publishing
patch.execute([{set: {publishedAt: new Date().toISOString()}}])
// Execute the actual publish operation
// This handles validation, draft management, etc.
publish.execute()
// Signal completion to the Studio
props.onComplete()
}
}
}
// In sanity.config.ts
import defaultResolve, {PublishAction} from 'sanity'
import {SetPublishDateAction} from './actions/SetPublishDateAction'
export default defineConfig({
// ...
document: {
actions: (prev, context) => {
return prev.map((Action) =>
Action === PublishAction ? SetPublishDateAction : Action
)
}
}
})useDocumentOperation gives you the primitives - The publish object from this hook has both publish.disabled (validation state) and publish.execute() (the operation itself)
disabled: publish.disabled is crucial - This preserves all the validation logic. If validation fails, the button is disabled automatically
publish.execute() does the heavy lifting - This handles the actual draft-to-published transition, all the edge cases, and the complexity you see in the source code
You're adding logic around the operation, not reimplementing it - Think of it as decorating the action, not replacing it
publish.disabled - This is how validation state flows throughdisabled with your own logic unless you have a very specific reasonprops.onComplete() - This signals the Studio that the action finishedisPublishing state properly - Watch props.draft to know when publishing completesFor many use cases (sending notifications, syncing to external systems, triggering webhooks), Sanity Functions are actually a better and more modern approach than extending document actions. They can listen to document changes without you having to touch the publish action at all:
// sanity.blueprint.ts
export default {
functions: [
{
name: 'on-publish',
handler: async (event) => {
if (event.type === 'document.published') {
// Your post-publish logic here
// Send notifications, update external systems, etc.
console.log('Document published:', event.document)
}
}
}
]
}This is cleaner, more maintainable, and doesn't risk breaking validation or other Studio behavior. Functions are the modern, recommended approach for reacting to content changes - they're serverless, automatically scaled, and completely separate from your Studio UI logic.
You're absolutely right to be concerned about testing! Document actions are notoriously hard to test because they're so tightly coupled to Studio's React context. Most developers:
The Studio team knows testing is a pain point - it's one reason why Functions are now recommended for many automation use cases, as they're easier to test in isolation and don't require a running Studio.
Don't reimplement - use the primitives from useDocumentOperation. The publish.disabled and publish.execute() from that hook encapsulate all the complexity you're seeing in the source code. Your custom action should:
{patch, publish} from useDocumentOperationdisabled: publish.disabledonHandle, do your custom logic, then call publish.execute()You're not missing anything fundamental - the API is genuinely tricky to work with because it's a low-level primitive that needs to handle many edge cases. The documentation could definitely be clearer about the wrapping pattern. But now you know: preserve publish.disabled, call publish.execute(), and let the platform handle the complexity.
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store