Document actions are the operations available in the document editor, such as publish, delete, and duplicate. You can create custom actions to extend document workflows.
What are document actions?
Document actions appear in the document toolbar and provide operations like:
- Publishing and unpublishing documents
- Deleting and duplicating documents
- Custom workflows (approval, scheduling, etc.)
- Integration with external services
Understanding the action component
Actions are React components that return action descriptions:
import {type DocumentActionComponent} from 'sanity'
const myAction: DocumentActionComponent = (props) => {
return {
label: 'My Action',
icon: RocketIcon,
onHandle: () => {
// Handle the action
},
}
}
Creating your first action
Define the action component
import {CheckmarkIcon} from '@sanity/icons'
import {type DocumentActionComponent} from 'sanity'
const approveAction: DocumentActionComponent = (props) => {
const {draft, published} = props
return {
label: 'Approve',
icon: CheckmarkIcon,
tone: 'positive',
onHandle: () => {
// Handle approval logic
console.log('Approving document:', draft?._id || published?._id)
},
}
}
Add it to your studio configuration:
import {defineConfig} from 'sanity'
export default defineConfig({
document: {
actions: (prev, context) => {
// Add to all documents
return [...prev, approveAction]
},
},
})
Show actions only for specific document types:
export default defineConfig({
document: {
actions: (prev, context) => {
if (context.schemaType === 'post') {
return [...prev, approveAction]
}
return prev
},
},
})
Action description properties
Actions return a description object with these properties:
interface DocumentActionDescription {
label: string // Button label
icon?: ComponentType | ReactNode // Icon component
title?: ReactNode // Tooltip text
tone?: ButtonTone // Visual style
disabled?: boolean // Disable the action
shortcut?: string // Keyboard shortcut
onHandle?: () => void // Click handler
dialog?: DocumentActionDialogProps // Dialog content
}
Built-in actions
Sanity provides these default actions:
publish - Publish the document
unpublish - Unpublish the document
delete - Delete the document
duplicate - Create a copy
discardChanges - Revert to published version
restore - Restore a deleted document
Replacing default actions
Replace a built-in action with a custom version:
import {type DocumentActionComponent} from 'sanity'
const MyPublishAction: DocumentActionComponent = (props) => {
return {
label: 'Publish Now',
icon: RocketIcon,
tone: 'primary',
onHandle: async () => {
// Custom publish logic
const {patch, publish} = props
// Add metadata before publishing
patch.execute([
{type: 'set', path: ['publishedBy'], value: 'current-user'},
{type: 'set', path: ['publishedAt'], value: new Date().toISOString()},
])
// Then publish
publish.execute()
},
}
}
MyPublishAction.action = 'publish'
export default defineConfig({
document: {
actions: (prev) =>
prev.map((action) =>
action.action === 'publish' ? MyPublishAction : action
),
},
})
Set the action property to identify which built-in action you’re replacing.
Actions with dialogs
Show confirmation dialogs before executing actions:
import {useState} from 'react'
import {type DocumentActionComponent} from 'sanity'
const deleteWithConfirm: DocumentActionComponent = (props) => {
const [dialogOpen, setDialogOpen] = useState(false)
return {
label: 'Delete',
icon: TrashIcon,
tone: 'critical',
onHandle: () => setDialogOpen(true),
dialog: dialogOpen && {
type: 'confirm',
tone: 'critical',
message: 'Are you sure you want to delete this document?',
onCancel: () => setDialogOpen(false),
onConfirm: () => {
props.onComplete()
// Execute delete logic
},
},
}
}
Dialog types
Sanity supports multiple dialog types:
Confirm dialog
Modal dialog
Popover dialog
dialog: {
type: 'confirm',
tone: 'critical',
message: 'Are you sure?',
onConfirm: () => {
// Handle confirmation
},
onCancel: () => {
// Handle cancellation
},
}
dialog: {
type: 'dialog',
header: 'Custom Dialog',
content: <MyCustomComponent />,
onClose: () => setDialogOpen(false),
width: 'medium',
}
dialog: {
type: 'popover',
content: (
<Box padding={4}>
<Text>Popover content</Text>
</Box>
),
onClose: () => setDialogOpen(false),
}
Using document props
Actions receive document state and utilities:
const myAction: DocumentActionComponent = (props) => {
const {
id, // Document ID
type, // Document type
draft, // Draft document
published, // Published document
patch, // Patch API
publish, // Publish API
unpublish, // Unpublish API
delete: del, // Delete API (renamed to avoid keyword)
} = props
return {
label: 'My Action',
onHandle: () => {
// Use document state
if (draft) {
console.log('Draft title:', draft.title)
}
},
}
}
Async actions
Handle asynchronous operations:
import {useState} from 'react'
const exportAction: DocumentActionComponent = (props) => {
const [isExporting, setIsExporting] = useState(false)
return {
label: isExporting ? 'Exporting...' : 'Export',
icon: DownloadIcon,
disabled: isExporting,
onHandle: async () => {
setIsExporting(true)
try {
const response = await fetch('/api/export', {
method: 'POST',
body: JSON.stringify(props.draft || props.published),
})
const blob = await response.blob()
// Download file
} finally {
setIsExporting(false)
}
},
}
}
Grouping actions
Organize actions into groups:
const myAction: DocumentActionComponent = (props) => {
return {
label: 'Custom Action',
group: 'paneActions',
onHandle: () => {},
}
}
Available groups:
default - Main action bar
paneActions - Secondary actions menu
Conditional actions
Show actions based on document state:
const approveAction: DocumentActionComponent = (props) => {
const {draft, published} = props
const status = draft?.status || published?.status
// Only show for pending documents
if (status !== 'pending') {
return null
}
return {
label: 'Approve',
icon: CheckmarkIcon,
onHandle: () => {
props.patch.execute([{
type: 'set',
path: ['status'],
value: 'approved',
}])
},
}
}
Keyboard shortcuts
Add keyboard shortcuts to actions:
const quickPublish: DocumentActionComponent = (props) => {
return {
label: 'Quick Publish',
shortcut: 'Ctrl+Shift+P',
onHandle: () => {
props.publish.execute()
},
}
}
Integration with external APIs
Integrate with external services:
const notifySlack: DocumentActionComponent = (props) => {
const [isSending, setIsSending] = useState(false)
return {
label: 'Notify Slack',
icon: BellIcon,
disabled: isSending,
onHandle: async () => {
setIsSending(true)
try {
await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
method: 'POST',
body: JSON.stringify({
text: `Document published: ${props.draft?.title}`,
}),
})
} finally {
setIsSending(false)
}
},
}
}
Add metadata for debugging:
const myAction: DocumentActionComponent = (props) => {
return {
label: 'My Action',
onHandle: () => {},
}
}
myAction.displayName = 'MyCustomAction'
Testing actions
Test actions in isolation:
import {renderHook} from '@testing-library/react'
test('approve action', () => {
const props = {
id: 'test-id',
type: 'post',
draft: {_id: 'drafts.test-id', status: 'pending'},
patch: {execute: jest.fn()},
}
const result = approveAction(props)
expect(result?.label).toBe('Approve')
expect(result?.onHandle).toBeDefined()
})
Best practices
- Use descriptive labels: Make action purpose clear
- Add icons: Visual cues improve UX
- Handle loading states: Show feedback for async operations
- Confirm destructive actions: Always confirm delete/unpublish
- Check permissions: Verify user has required access
- Provide feedback: Show success/error messages
Use the tone property to visually indicate action severity: positive, caution, critical, or primary.
Next steps