How to extend default Publish action while preserving validation and functionality?
I 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 Core Pattern: Wrapping, Not Replacing
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
)
}
}
})What the Default Publish Action Actually Does
The default publish action handles several critical things that you don't want to reimplement:
- Validation checking - It uses the
disabledproperty to prevent publishing when validation fails - Draft management - It knows how to handle the
drafts.prefix and the relationship between draft and published documents - Document operations - It calls useDocumentOperation properly under the hood
- UI state - It manages loading states, disabled states, and provides user feedback
- Error handling - It handles edge cases you might not think of
When you see publish.disabled in action code, that's checking validation state. When you see publish.execute(), that's the actual operation being performed.
A Complete Working Example
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
)
}
}
})The Key Concepts You Need to Understand
useDocumentOperationgives you the primitives - Thepublishobject from this hook has bothpublish.disabled(validation state) andpublish.execute()(the operation itself)disabled: publish.disabledis crucial - This preserves all the validation logic. If validation fails, the button is disabled automaticallypublish.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 codeYou're adding logic around the operation, not reimplementing it - Think of it as decorating the action, not replacing it
Common Pitfalls to Avoid
- Don't try to recreate the publish logic from scratch - You'll miss validation, draft handling, and edge cases
- Always use
publish.disabled- This is how validation state flows through - Don't override
disabledwith your own logic unless you have a very specific reason - Call
props.onComplete()- This signals the Studio that the action finished - Handle the
isPublishingstate properly - Watchprops.draftto know when publishing completes
Alternative: Sanity Functions for Post-Publish Actions
For 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.
On Testing
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:
- Extract business logic into separate, testable functions - Keep the action wrapper thin
- Test the business logic in isolation - Unit test your field update logic, notification logic, etc.
- Do manual QA on the action integration - Test in the Studio itself
- Use TypeScript - Catch type errors at compile time
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.
The Key Takeaway
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:
- Get
{patch, publish}fromuseDocumentOperation - Return an action object with
disabled: publish.disabled - In
onHandle, do your custom logic, then callpublish.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.
Show original thread16 replies
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.