v3.71.0 (2026-01-13)
🚀 Features
- supersedes option to job queue concurrency controls (#15179) (9aeb843)
- add support for custom Status component in document controls (#11154) (ef863e6)
- add exclusive concurrency controls for workflows and tasks (#15177) (35fe09d)
- add bulkOperations.singleTransaction config option (#14387) (92da9fa)
- add support for additional IANA timezones, custom UTC offsets and overriding the timezone field (#15120) (a8785ba)
- ability to cancel current job from within workflow or task handlers (#15119) (1bd3146)
- add typescript.strictDraftTypes flag for opt-in draft query type safety (#14388) (58faafd)
- drizzle: include collection and global slugs in validation errors (#15147) (910d274)
- plugin-ecommerce: new hooks, cart logic moved to the server and fixed several bugs (#15142) (dcd4030)
- plugin-ecommerce: add new method refreshCart in useCart (#14765) (#14767) (529726e)
- plugin-import-export: refactor plugin and add import functionality (#14782) (13e6035)
- plugin-mcp: add draft parameter support to MCP find resource tool (#14924) (744a593)
- plugin-mcp: adds tools that can find and update globals (#15091) (f6d9873)
- plugin-nested-docs: add req parameter to GenerateURL and GenerateLabel types in nested docs (#14617) (6821a09)
- plugin-redirects: add Japanese translations (#15080) (c5b57f7)
- plugin-search: enables skipping of document syncing (#14928) (dfcf15c)
- sdk: automatically fallback to generated types attempt (#15167) (ae50d39)
- sdk: add proper error handling (#15148) (bfe2154)
Feature Details
Job Queue Concurrency Supersedes - Newer jobs can automatically delete older pending jobs with the same concurrency key. Enables "last queued wins" behavior for scenarios where only the latest state matters. #15179
concurrency: {
key: ({ input }) => `generate:${input.documentId}`,
exclusive: true,
supersedes: true, // Newer jobs delete older pending ones (not yet completed and did not start processing yet)
}Exclusive Concurrency Controls - Prevents race conditions when multiple jobs operate on the same resource. Jobs with the same concurrency key will not run in parallel. Requires enableConcurrencyControl: true (will default to true in v4.0). #15177
export default buildConfig({
jobs: {
enableConcurrencyControl: true,
workflows: [{
slug: 'syncDocument',
concurrency: ({ input }) => `sync:${input.documentId}`,
handler: async ({ job }) => {
// Only one job per documentId runs at a time
}
}]
}
})Job Cancellation from Handlers - Throw JobCancelledError from within a task or workflow handler to stop the job without retrying. #15119
Custom Status Component - Replace the Status section in document or global edit views without replacing the entire Edit view. Useful for custom locale publishing logic or additional status indicators. #11154
admin: {
components: {
edit: {
Status: '/components/Status/index.tsx#Status',
},
},
},Bulk Operations Single Transaction (db-mongodb) - Handle database transaction limitations when processing large numbers of documents in bulk operations. Useful for DocumentDB and Cosmos DB which have cursor limitations within transactions. #14387
Additional IANA Timezones & Custom UTC Offsets - Support for additional IANA timezone names via DateTimeFormat API validation, custom UTC offsets in ±HH:mm format, and the ability to override the timezone field configuration. #15120
{
name: 'eventTime',
type: 'date',
timezone: {
supportedTimezones: [
{ label: 'UTC+5:30 (India)', value: '+05:30' },
{ label: 'UTC-8 (Pacific)', value: '-08:00' },
{ label: 'UTC+0', value: '+00:00' },
],
},
}Override the timezone field:
{
name: 'publishedAt',
type: 'date',
label: 'Published At',
timezone: {
override: ({ baseField }) => ({
...baseField,
admin: {
...baseField.admin,
disableListColumn: true, // Hide from list view columns
},
}),
},
}Strict Draft Types (typescript) - Opt-in strictDraftTypes flag for correct type safety when querying drafts. When enabled, find operations with draft: true will correctly type required fields as optional. Will become default in v4.0. #14388
export default buildConfig({
typescript: {
strictDraftTypes: true, // defaults to false
},
})Validation Error Context (drizzle) - Unique constraint ValidationErrors now include data.collection or data.global for better error context when debugging. #15147
Server-Side Cart Logic (plugin-ecommerce) - Cart logic moved to the server with new REST API endpoints. New hooks: onLogin (merge guest cart with user cart), onLogout (clear session), clearSession, mergeCart, and refreshCart. Support for custom cart item matchers and MongoDB-style $inc operator for quantity changes. #15142
/**
* Custom cart item matcher that includes fulfillment option.
*/
const fulfillmentCartItemMatcher: CartItemMatcher = ({ existingItem, newItem }) => {
const existingProductID =
typeof existingItem.product === 'object' ? existingItem.product.id : existingItem.product
const existingVariantID =
existingItem.variant && typeof existingItem.variant === 'object'
? existingItem.variant.id
: existingItem.variant
const productMatches = existingProductID === newItem.product
const variantMatches = newItem.variant
? existingVariantID === newItem.variant
: !existingVariantID
const existingFulfillment = existingItem.fulfillment as string | undefined
const newFulfillment = newItem.fulfillment as string | undefined
const fulfillmentMatches = existingFulfillment === newFulfillment
return productMatches && variantMatches && fulfillmentMatches
}refreshCart Method (plugin-ecommerce) - Manually refresh cart state after direct modifications, allowing the UI to stay in sync without being blocked by addItem's uniqueness validation. #14767
Import Functionality (plugin-import-export) - Complete plugin refactor with new import functionality. Config is now per-collection with required collections array. Supports disabling import/export per collection and custom collection overrides. #14782 ⚠️ BREAKING CHANGE
importExportPlugin({
overrideExportCollection: (collection) => {
collection.admin.group = 'System'
collection.upload.staticDir = path.resolve(dirname, 'uploads')
return collection
},
overrideImportCollection: (collection) => {
collection.admin.group = 'System'
collection.upload.staticDir = path.resolve(dirname, 'uploads')
return collection
},
collections: [
{
slug: 'posts',
import: false, // disables import functionality, export enabled by default
},
{
slug: 'pages',
export: ({ collection }) => {
collection.admin.group = 'System'
collection.upload.staticDir = path.resolve(dirname, 'uploads')
return collection
},
disableJobsQueue: true, // disable jobs queue for this collection only
},
],
debug: true,
})Draft Parameter for MCP Find (plugin-mcp) - Query draft/unpublished documents via the MCP plugin's find tool using the new draft boolean parameter. #14924
Globals Support (plugin-mcp) - New MCP tools to find and update globals. #15091
Request Parameter in Nested Docs (plugin-nested-docs) - req parameter added to generateURL and generateLabel functions for more flexibility (e.g., reading current locale). #14617
Skip Sync (plugin-search) - Conditionally skip syncing documents to the search index based on locale, document properties, or other criteria. #14928
skipSync: async ({ locale, doc, collectionSlug, req }) => {
if (!locale) return false
const tenant = await req.payload.findByID({
collection: 'tenants',
id: doc.tenant.id,
})
return !tenant.allowedLocales.includes(locale)
}Automatic Type Inference (sdk) - The SDK automatically uses your generated types via module augmentation—no need to manually pass GeneratedTypes. #15167
import { PayloadSDK } from '@payloadcms/sdk'
const sdk = new PayloadSDK({}) // Types inferred automatically from payload-types.tsProper Error Handling (sdk) - The SDK now throws PayloadSDKError on failed API requests with status, errors, response, and message properties. #15148
import { PayloadSDKError } from '@payloadcms/sdk'
try {
await sdk.create({ collection: 'posts', data: { ... } })
} catch (err) {
if (err instanceof PayloadSDKError) {
console.log(err.status) // 400
console.log(err.errors) // [{ name: 'ValidationError', message: '...', data: {...} }]
}
}Japanese Translations (plugin-redirects) - Localized admin UI strings for Japanese users. #15080
🐛 Bug Fixes
- wrong construction of urlToUse leads to false alert with logger.error (#15190) (ec6bba5)
- adds missing transactions to login and logout operations (#15134) (dd494be)
- set basePath from next config as env variable (#15154) (a8bfade)
- throw error on empty relationTo array and allow disabling lockDocuments on all collections (#14871) (9521ec6)
- add distinct validate to richtext field type definition (#15069) (d68e75a)
- exclude files from being sent to the form-state action (#15174) (aaea133)
- full image urls stored in DB (#15089) (d462f9b)
- field schema map paths (#10852) (d288752)
- custom OPTIONS endpoints are intercepted and cannot set custom CORS headers (#15153) (c103667)
- upload drawer not loading data for uploads without files (#15150) (00fb6e8)
- cannot read private member #headers error on Node.js 24 when using isolateObjectProperty (#15116) (e214deb)
- db-mongodb: avoid unnecessary
$lookupwhen a join field is not selected (#15149) (e39b1b5) - db-mongodb:
existsoperator on fields that have an array value in the db (#15152) (0afe200) - db-mongodb: find id field from flattened fields (#15110) (40081f4)
- db-postgres: add filenameCompoundIndex baseIndexes for upload collections (#15182) (01f90c9)
- db-postgres: localized and hasMany/polymorphic relationships/uploads inside blocks (#15095) (01e4412)
- drizzle: unique field errors were not thrown as ValidationErrors (#15146) (43c19cb)
- live-preview: remove payload import (#15160) (ec658a4)
- plugin-cloud-storage: should persist external data returned by handleUpload (#15188) (7d80d21)
- plugin-cloud-storage: prevent deleting original file when duplicating (#14961) (9d54267)
- plugin-mcp: handle defaultDepth: 0 in api key authentication (#15014) (e0e058c)
- plugin-multi-tenant: cannot clear selected tenant from dashboard view (#15141) (cd77e5d)
- richtext-lexical,ui: make uploadNode default to
altuser-defined values. (#15097) (3290b04) - storage-*: add range headers to storage adapters (#14890) (ef90811)
- storage-s3: respect upload limits with client uploads (#15176) (4a6189e)
- templates: fix mobile menu auth buttons overflow in ecommerce template (#15020) (6c1109f)
- templates: improve cli detection in cloudflare template (#15098) (5d21ed1)
- templates: add recommended serverExternalPackages to cloudflare (#15094) (0eed581)
- translations: improve Japanese translations for naturalness and consistency (#15077) (88a901c)
- ui: ensure up-to-date upload preview thumbnails in admin panel (#15029) (6f4b272)
- ui: use optional chaining for potentiallyStalePath (#15185) (38cd67a)
- ui: virtual fields with virtual: true show sort chevrons in list view (#15186) (33c3630)
- ui: prevent text overlap in breadcrumbs (#15040) (0e27f19)
- ui: avoid creating hasMany options during IME composition (#15086) (98bc876)
- ui: duplicate document has unused serverURL dependency (#15178) (6d212b8)
- ui: bulk upload error count (#15155) (0a46f4e)
- ui: margins on small screens when using hideGutter in group (#15041) (22f94cd)
- ui: preserve beforeInput/afterInput components in bulk edit (#14954) (c62dc2a)
- ui: prevent wrong scroll target when adding rows in repeated array blocks (#15047) (f13a741)
- ui: crop width and height inputs limited to -1 of max (#15101) (3a6d3bd)
⚡ Performance
- optimized collection create when versions are enabled (#14885) (25813a7)
- graphql: optimized join count when docs are not needed (#14872) (6025eff)
🛠 Refactors
- clean up generated type resolution (#15163) (51d6cd2)
- deprecate returning failed state from job queue handlers (#15102) (070ded7)
📚 Documentation
- clarify available operations for beforeOperation hook (#14774) (8c59762)
- update link for MDN docs on cookies in requests (#15075) (0859f1c)
- updates custom upload component examples (#14678) (d3d8b4e)
- add graphql multi-field sort note (#15123) (14ac061)
- task failure docs (#15096) (81df3d6)
🧪 Tests
- remove silly tests from accessComposition unit tests (#15158) (cc33129)
- fixes flaky form state test (#15128) (251fc68)
- adds assert helpers to the a11y suite (#15132) (daf8061)
- running a11y test suite sequentially (#15130) (536582f)
- update outdated hardcoded postgres connection strings (#15114) (7930f44)
- fixes lexical block test flakes (#15113) (13edb8a)
⚙️ CI
- remove website post release notification (#15133) (5ebda61)
- deps: bumps setup-node version from 4 to 6 (#15122) (fe9119b)
🏡 Chores
- deprecate skip parameter from db-adapters (#15183) (ea15f00)
- add v4 comments for relationTo types (#15187) (d7351d5)
- upgrade to pnpm 10 (#15137) (1c8d515)
- refactor withPayload back to js (#15159) (44af1ad)
- plugin-mcp: adds config.admin.user as an optional default userCollection (#14989) (cdabf79)
- templates: replace arbitrary tailwind values with utilities in ecommerce template (#14654) (67cc734)
⚠️ BREAKING CHANGES
-
plugin-import-export: refactor plugin and add import functionality (#14782) (13e6035)
This PR works to refactor the entire plugin so it's easier for us to
work with it going forward and also implements import functionality.
🤝 Contributors
- Sasha (@r1tsuu)
- Jeffery To (@jefferyto)
- Alessio Gravili (@AlessioGr)
- tapartdev (@tapartdev)
- Paul (@paulpopus)
- Patrik (@PatrikKozak)
- Elmo (@Spreizu)
- Jarrod Flesch (@JarrodMFlesch)
- German Jablonski (@GermanJablo)
- Jessica Rynkar (@jessrynkar)
- roboin (@Robot-Inventor)
- Tobias Odendahl (@tak-amboss)
- Rick Geersing (@RickGeersing)
- Dan Ribbens (@DanRibbens)
- Riley Langbein (@rilrom)
- David Wensley (@wensleyio)
- Bernhard Frick (@baer95)
- Said Akhrarov (@akhrarovsaid)
- Kendell (@kendelljoseph)
- Jiayi Wang (@Jeromestein)
- Jake (@jacobsfletch)
- Jens Becker (@jhb-dev)
- Sean Zubrickas (@zubricks)
- Anton Timmermans (@atimmer)
- Ricardo Tavares (@rjgtav)
- Marcin Gierada (@teastudiopl)
- Colum Kelly (@columk1)
- Elliot DeNolf (@denolfe)
- Jonathan Elmgren (@jonathanelmgren)
- Thomas Coldwell (@thomas-coldwell)