Expanded Plugin API β New definePlugin helper introduces opt-in execution ordering, cross-plugin discovery via a slug-keyed plugins map, and module augmentation for type-safe plugin options. The existing (config) => config contract remains unchanged. #16247
import { definePlugin } from 'payload'
export const seoPlugin = definePlugin<SEOPluginOptions>({
slug: 'plugin-seo',
order: 10,
plugin: ({ config, plugins, collections, generateTitle }) => ({
...config,
// collections and generateTitle come from SEOPluginOptions
}),
})
Profiling Utilities β Lightweight timeSync and timeAsync wrappers for measuring function execution time during development. Wrap any function to capture its duration, then call printProfileResults for a formatted timing table. Not intended for production use. #16198
Internal Plugin Priority & Slug API β Plugins can now attach priority, slug, and options properties for execution ordering and cross-plugin discovery. Lower priority runs first; other plugins can find each other by slug via config.plugins without imports. Marked @internal for now. #16244
Hidden Slug Field Buttons on Read-Only β The Generate and Lock/Unlock buttons on slug fields are now automatically hidden when the field is read-only, removing controls that serve no purpose in that state. #14824
Agent Flag for CPA (cpa) β create-payload-app now supports a --agent / -a flag (claude, codex, cursor) that downloads the Payload coding skill from GitHub and installs it in the correct directory for your agent. A root-level CLAUDE.md or AGENTS.md is written for discoverability. Use --no-agent to skip. #16278
UUIDv7 Support (drizzle) β New idType: 'uuidv7' option for Postgres and SQLite adapters generates time-ordered UUIDs that are friendlier for B-tree indexes than random v4 UUIDs, while using the same storage column type. IDs are generated in application code so older Postgres versions are supported. #16113
Custom Email Headers (email-resend) β The Resend adapter now passes custom headers from sendEmail options to the Resend API, enabling features like List-Unsubscribe headers that were previously silently dropped. #15645
await payload.sendEmail({
from: "Test <test@domain.com>",
to: "jimmybillbob@example.com",
subject: "Email with custom headers",
html: html,
headers: {
"List-Unsubscribe": "<https://domain.com/unsubscribe>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
},
});
Custom Collection Views (next) β Register custom views at the collection level via admin.components.views[key] with a Component and path. Folders take routing precedence over custom views on upload collections. #16243
{
slug: 'products',
admin: {
components: {
views: {
grid: {
Component: '/components/GridView',
path: '/grid',
exact: true,
},
},
},
},
}
Checkbox Label Clarity (plugin-form-builder) β The form builder checkbox field label was changed from "Default Value" to "Checked by default" to eliminate confusion about whether the checkbox toggles a default value or sets the initial checked state. #15229
Extensible MCP Plugin (plugin-mcp) β External plugins can now extend plugin-mcp by finding it via slug in config.plugins and injecting custom MCP tools into its options. Also exports the MCPPluginConfig type for type-safe tool injection. #16245
View Override System for Custom Node Rendering (richtext-lexical) β β οΈ Experimental. Override how any Lexical node type is rendered in the editor via view maps. Supports custom DOM, React components, or HTML strings. Works in both the admin editor and frontend JSX serialization for WYSIWYG consistency. #14244
export const myViews: LexicalEditorViewMap = {
default: {
heading: {
createDOM() {
const h2 = document.createElement('h2')
h2.textContent = 'Custom Heading'
return h2
},
},
horizontalRule: {
Component: () => <div className="custom-hr">---</div>,
},
link: {
html: '<a href="#">Custom Link</a>',
},
},
}
{
fields: [
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
views: '/path/to/views.tsx#myViews',
}),
},
]
}
Composite Prefixes for Storage Adapters (storage-*) β New useCompositePrefixes option combines collection and document prefixes instead of one overriding the other. Also fixes a bug where client uploads ignored document prefix entirely. Applies to S3, Azure, GCS, R2, and Vercel Blob. #16230
| Mode | Collection Prefix | Doc Prefix | Result |
|------|------------------|------------|--------|
| false (default) | media-folder | user-123 | user-123/file.jpg |
| true | media-folder | user-123 | media-folder/user-123/file.jpg |
undefined fields (#16272) (4b4d61c)position (#14390) (d9b3c07)drizzle-orm to 0.45.2 to resolve an SQL injection vulnerability and pg to 8.20.0 (#16168) (af1a932)getPrimaryDb is used for all write operations (#16240) (aa44649)beforeSync hook (#11581) (ebbd6ad)s3 and vercel-blob (#16115) (a6735f3)APIError to hooks overview (#13047) (d03054e)basePath was not respected (#16084) (3c40241)alwaysInsertFields and add comprehensive integration test suite with a vercel blob emulator (#16080) (8530b45)tokenInMemory not set after refreshing cookie (#15928) (17266ab)admin.dateFormat for list view filters (#16040) (d5fe0ce)disableUnique property to the slug field for better multi tenant plugin support (#15963) (395e1ed)near query can give incorrect results (#15907) (843306c)forceInlineBlocks property to use in plugin mcp (#15892) (6a9e367)sanitizeWhereQuery for join query access result (#15891) (dc049fe)payload generate:db-schema with circular references (#15895) (66a2efa)Separate Block Icon Configuration (richtext-lexical) β Configure different images for Lexical block icons and block drawer thumbnails independently. Previously, imageURL served both contexts, forcing a compromise between a good 20x20px icon and a good drawer thumbnail. The new images property supports separate icon and thumbnail values with automatic fallback. Fully backwards compatible β imageURL still works but is deprecated. #15632
const QuoteBlock: Block = {
slug: 'quote',
images: {
icon: 'https://example.com/icons/quote-20x20.svg',
thumbnail: { url: 'https://example.com/thumbnails/quote-480x320.jpg', alt: 'Quote block' },
},
fields: [...],
}
Lexical Upgrade 0.35.0 β 0.41.0 (richtext-lexical) β Upgrades the Lexical rich text editor dependency from v0.35.0 to v0.41.0. Includes upstream fixes like normalizeMarkdown (facebook/lexical#7812). All Lexical breaking changes are handled internally by Payload β no action required for standard usage. If you installed lexical manually, update it to 0.41.0 (though using the re-exported versions from @payloadcms/richtext-lexical/lexical/* is recommended). #15760
Modular Dashboard Translations (translations) β Adds i18n translation support for the Modular Dashboards feature, covering all dashboard widget buttons and error messages. Previously, dashboard UI elements lacked translation keys, making them inaccessible for non-English users. Also updates the automatic translation script to use GPT-4.1 for improved cost efficiency. #15004
<img width="830" height="399" alt="image" src="https://github.com/user-attachments/assets/ca6cd857-e0dd-47b0-8b36-6242396f2c73" />rename widget ComponentPath to Component for consistency (#15780) (f7d0d04)
Widget.ComponentPath to Widget.Component and types it as PayloadComponentinstead ofstring`PayloadComponentPayloadComponent contextual type detection - string-typed properties were invisible to it)TypeScript Plugin for Component Paths - New @payloadcms/typescript-plugin validates PayloadComponent import paths directly in your IDE. It checks that referenced files and exports exist, provides autocomplete for file paths and export names, supports go-to-definition on component path strings, and understands all Payload path conventions including absolute paths, relative paths, tsconfig aliases, and package imports. #15779
https://github.com/user-attachments/assets/2a8a8f73-a8fd-4e79-9dc4-795d817b5b75
pnpm add -D @payloadcms/typescript-plugin
{
"compilerOptions": {
"plugins": [{ "name": "next" }, { "name": "@payloadcms/typescript-plugin" }]
}
}
Trash Out of Beta with Granular Delete Access - Trash is now a stable feature. Delete access control can now distinguish between trashing and permanently deleting β allowing you to permit users to soft-delete documents while restricting permanent deletion to admins. When data.deletedAt is being set, the operation is a trash; otherwise it's a permanent delete. #15210
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
trash: true,
access: {
delete: ({ req: { user }, data }) => {
// Not logged in - no access
if (!user) {
return false
}
// Admins can do anything (trash or permanently delete)
if (user.roles?.includes('admin')) {
return true
}
// Regular users: check what operation they're attempting
// If data.deletedAt is being set, it's a trash operation - allow it
if (data?.deletedAt) {
return true
}
// Otherwise it's a permanent delete - deny for non-admins
return false
},
},
fields: [
// ...
],
}
Widget Fields (next, ui) - Dashboard widgets can now declare configurable fields, similar to Blocks. Widget data is editable from a new drawer UI when in dashboard editing mode. Full type generation is included β WidgetInstance<T> is generic with typed data and width, and WidgetServerProps is generic so widget components receive typed widgetData. #15700
https://github.com/user-attachments/assets/b24d3635-8635-4d5f-84af-a6dcf801aa4f
import { buildConfig } from 'payload'
export default buildConfig({
admin: {
dashboard: {
widgets: [
{
slug: 'sales-summary',
ComponentPath: './components/SalesSummary.tsx#default',
fields: [
{ name: 'title', type: 'text' },
{
name: 'timeframe',
type: 'select',
options: ['daily', 'weekly', 'monthly', 'yearly'],
},
{ name: 'showTrend', type: 'checkbox' },
],
minWidth: 'small',
maxWidth: 'medium',
},
],
},
},
})
import type { WidgetServerProps } from 'payload'
import type { SalesSummaryWidget } from '../payload-types'
export default async function SalesSummaryWidgetComponent({
widgetData,
}: WidgetServerProps<SalesSummaryWidget>) {
const title = widgetData?.title ?? 'Sales Summary'
const timeframe = widgetData?.timeframe ?? 'monthly'
return (
<div className="card">
<h3>
{title} ({timeframe})
</h3>
</div>
)
}
MCP Plugin Out of Beta (plugin-mcp) - @payloadcms/plugin-mcp is now stable and ready for production use. #15711
Virtual Field Filtering in MCP (plugin-mcp) - Virtual fields (virtual: true) are now automatically stripped from MCP tool input schemas and filtered from parsed data before create, update, and updateGlobal operations. This prevents non-stored fields from appearing as accepted MCP parameters. #15680
Markdown Transformer for Upload Nodes (richtext-lexical) - Upload nodes are now properly converted when using convertLexicalToMarkdown. Previously, upload nodes were silently dropped during markdown conversion. Now populated image uploads output , non-image uploads output link syntax, and non-populated uploads output a reference placeholder so data is never lost. #15630
Dashed Button Style (ui) - Adds a new dashed button style variant. Also replaces box-shadow with border on all buttons and fixes icon-only button padding. #15728
Editable Query Presets from Form View (ui) - Query presets can now be created and edited directly from the document form view using a full WhereBuilder, column picker, and groupBy selector β no longer requiring the list view to build queries first. #15657
https://github.com/user-attachments/assets/d25b447b-c942-47ae-8878-47bd17a8a7fb