Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when working with Payload projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
.claude/skills/asymmetric-al-payload/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 168% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 300% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 230% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 660% | 0% |
Payload is a Next.js native CMS with TypeScript-first architecture, providing admin panel, database management, REST/GraphQL APIs, authentication, and file storage.
| Task | Solution | Details | | ------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | Auto-generate slugs | slugField() | FIELDS.md#slug-field-helper | | Restrict content by user | Access control with query | ACCESS-CONTROL.md#row-level-security-with-complex-queries | | Local API user ops | user + overrideAccess: false | QUERIES.md#access-control-in-local-api | | Draft/publish workflow | versions: { drafts: true } | COLLECTIONS.md#versioning--drafts | | Computed fields | virtual: true with afterRead | FIELDS.md#virtual-fields | | Conditional fields | admin.condition | FIELDS.md#conditional-fields | | Custom field validation | validate function | FIELDS.md#validation | | Filter relationship list | filterOptions on field | FIELDS.md#relationship | | Select specific fields | select parameter | QUERIES.md#field-selection | | Auto-set author/dates | beforeChange hook | HOOKS.md#collection-hooks | | Prevent hook loops | req.context check | HOOKS.md#context | | Cascading deletes | beforeDelete hook | HOOKS.md#collection-hooks | | Geospatial queries | point field with near/within | FIELDS.md#point-geolocation | | Reverse relationships | join field type | FIELDS.md#join-fields | | Next.js revalidation | Context control in afterChange | HOOKS.md#nextjs-revalidation-with-context-control | | Query by relationship | Nested property syntax | QUERIES.md#nested-properties | | Complex queries | AND/OR logic | QUERIES.md#andor-logic | | Transactions | Pass req to operations | ADAPTERS.md#threading-req-through-operations | | Background jobs | Jobs queue with tasks | ADVANCED.md#jobs-queue | | Custom API routes | Collection custom endpoints | ADVANCED.md#custom-endpoints | | Cloud storage | Storage adapter plugins | ADAPTERS.md#storage-adapters | | Multi-language | localization config + localized: true | ADVANCED.md#localization | | Create plugin | (options) => (config) => Config | PLUGIN-DEVELOPMENT.md#plugin-architecture | | Plugin package setup | Package structure with SWC | PLUGIN-DEVELOPMENT.md#plugin-package-structure | | Add fields to collection | Map collections, spread fields | PLUGIN-DEVELOPMENT.md#adding-fields-to-collections | | Plugin hooks | Preserve existing hooks in array | PLUGIN-DEVELOPMENT.md#adding-hooks | | Check field type | Type guard functions | FIELD-TYPE-GUARDS.md |
bashnpx create-payload-app@latest my-app cd my-app pnpm dev
tsimport { buildConfig } from 'payload' import { mongooseAdapter } from '@payloadcms/db-mongodb' import { lexicalEditor } from '@payloadcms/richtext-lexical' import path from 'path' import { fileURLToPath } from 'url' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) export default buildConfig({ admin: { user: 'users', importMap: { baseDir: path.resolve(dirname), }, }, collections: [Users, Media], editor: lexicalEditor(), secret: process.env.PAYLOAD_SECRET, typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), }, db: mongooseAdapter({ url: process.env.DATABASE_URL, }), })
tsimport type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title', defaultColumns: ['title', 'author', 'status', 'createdAt'], }, fields: [ { name: 'title', type: 'text', required: true }, { name: 'slug', type: 'text', unique: true, index: true }, { name: 'content', type: 'richText' }, { name: 'author', type: 'relationship', relationTo: 'users' }, ], timestamps: true, }
For more collection patterns (auth, upload, drafts, live preview), see COLLECTIONS.md.
ts// Text field { name: 'title', type: 'text', required: true } // Relationship { name: 'author', type: 'relationship', relationTo: 'users', required: true } // Rich text { name: 'content', type: 'richText', required: true } // Select { name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' } // Upload { name: 'image', type: 'upload', relationTo: 'media' }
For all field types (array, blocks, point, join, virtual, conditional, etc.), see FIELDS.md.
tsexport const Posts: CollectionConfig = { slug: 'posts', hooks: { beforeChange: [ async ({ data, operation }) => { if (operation === 'create') { data.slug = slugify(data.title) } return data }, ], }, fields: [{ name: 'title', type: 'text' }], }
For all hook patterns, see HOOKS.md. For access control, see ACCESS-CONTROL.md.
tsimport type { Access } from 'payload' import type { User } from '@/payload-types' // Type-safe access control export const adminOnly: Access = ({ req }) => { const user = req.user as User return user?.roles?.includes('admin') || false } // Row-level access control export const ownPostsOnly: Access = ({ req }) => { const user = req.user as User if (!user) return false if (user.roles?.includes('admin')) return true return { author: { equals: user.id }, } }
ts// Local API const posts = await payload.find({ collection: 'posts', where: { status: { equals: 'published' }, 'author.name': { contains: 'john' }, }, depth: 2, limit: 10, sort: '-createdAt', }) // Query with populated relationships const post = await payload.findByID({ collection: 'posts', id: '123', depth: 2, // Populates relationships (default is 2) }) // Returns: { author: { id: "user123", name: "John" } } // Without depth, relationships return IDs only const post = await payload.findByID({ collection: 'posts', id: '123', depth: 0, }) // Returns: { author: "user123" }
For all query operators and REST/GraphQL examples, see QUERIES.md.
ts// In API routes (Next.js) import { getPayload } from 'payload' import config from '@payload-config' export async function GET() { const payload = await getPayload({ config }) const posts = await payload.find({ collection: 'posts', }) return Response.json(posts) } // In Server Components import { getPayload } from 'payload' import config from '@payload-config' export default async function Page() { const payload = await getPayload({ config }) const { docs } = await payload.find({ collection: 'posts' }) return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div> }
By default, Local API operations bypass ALL access control, even when passing a user.
ts// ❌ SECURITY BUG: Passes user but ignores their permissions await payload.find({ collection: 'posts', user: someUser, // Access control is BYPASSED! }) // ✅ SECURE: Actually enforces the user's permissions await payload.find({ collection: 'posts', user: someUser, overrideAccess: false, // REQUIRED for access control })
When to use each:
overrideAccess: true (default) - Server-side operations you trust (cron jobs, system tasks)overrideAccess: false - When operating on behalf of a user (API routes, webhooks)See QUERIES.md#access-control-in-local-api.
Nested operations in hooks without req break transaction atomicity.
ts// ❌ DATA CORRUPTION RISK: Separate transaction hooks: { afterChange: [ async ({ doc, req }) => { await req.payload.create({ collection: 'audit-log', data: { docId: doc.id }, // Missing req - runs in separate transaction! }) }, ] } // ✅ ATOMIC: Same transaction hooks: { afterChange: [ async ({ doc, req }) => { await req.payload.create({ collection: 'audit-log', data: { docId: doc.id }, req, // Maintains atomicity }) }, ] }
See ADAPTERS.md#threading-req-through-operations.
Hooks triggering operations that trigger the same hooks create infinite loops.
ts// ❌ INFINITE LOOP hooks: { afterChange: [ async ({ doc, req }) => { await req.payload.update({ collection: 'posts', id: doc.id, data: { views: doc.views + 1 }, req, }) // Triggers afterChange again! }, ] } // ✅ SAFE: Use context flag hooks: { afterChange: [ async ({ doc, req, context }) => { if (context.skipHooks) return await req.payload.update({ collection: 'posts', id: doc.id, data: { views: doc.views + 1 }, context: { skipHooks: true }, req, }) }, ] }
See HOOKS.md#context.
txtsrc/ ├── app/ │ ├── (frontend)/ │ │ └── page.tsx │ └── (payload)/ │ └── admin/[[...segments]]/page.tsx ├── collections/ │ ├── Posts.ts │ ├── Media.ts │ └── Users.ts ├── globals/ │ └── Header.ts ├── components/ │ └── CustomField.tsx ├── hooks/ │ └── slugify.ts └── payload.config.ts
ts// payload.config.ts export default buildConfig({ typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), }, // ... }) // Usage import type { Post, User } from '@/payload-types'
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 12,736 | 7,716 | -39% | 1 | 1 | 0% | 2,340 | 5,294 | +126% | 0 | 0 | — |
case-02 | pass→pass | 6,686 | 4,665 | -30% | 1 | 1 | 0% | 1,178 | 4,717 | +300% | 0 | 0 | — |
case-03 | pass→pass | 9,613 | 8,935 | -7% | 1 | 1 | 0% | 1,662 | 5,489 | +230% | 0 | 0 | — |
case-04 | fail→pass | 11,064 | 7,301 | -34% | 1 | 1 | 0% | 1,919 | 5,146 | +168% | 0 | 0 | — |
case-05 | pass→pass | 3,494 | 3,516 | +1% | 1 | 1 | 0% | 581 | 4,418 | +660% | 0 | 0 | — |
case-06 | pass→pass | 9,724 | 5,915 | -39% | 1 | 1 | 0% | 1,661 | 4,901 | +195% | 0 | 0 | — |
case-07 | pass→pass | 8,379 | 3,883 | -54% | 1 | 1 | 0% | 1,549 | 4,502 | +191% | 0 | 0 | — |
case-08 | pass→pass | 8,440 | 6,196 | -27% | 1 | 1 | 0% | 1,494 | 4,893 | +228% | 0 | 0 | — |
case-09 | pass→pass | 6,913 | 5,491 | -21% | 1 | 1 | 0% | 1,206 | 4,774 | +296% | 0 | 0 | — |
case-10 | pass→pass | 4,112 | 2,653 | -35% | 1 | 1 | 0% | 697 | 4,248 | +509% | 0 | 0 | — |
case-11 | pass→pass | 9,384 | 5,304 | -43% | 1 | 1 | 0% | 1,620 | 4,736 | +192% | 0 | 0 | — |
case-12 | pass→pass | 6,673 | 4,084 | -39% | 1 | 1 | 0% | 1,068 | 4,532 | +324% | 0 | 0 | — |
case-13 | pass→pass | 8,446 | 7,299 | -14% | 1 | 1 | 0% | 1,556 | 5,238 | +237% | 0 | 0 | — |
case-14 | pass→pass | 6,295 | 3,700 | -41% | 1 | 1 | 0% | 1,033 | 4,485 | +334% | 0 | 0 | — |
case-15 | pass→pass | 7,700 | 5,795 | -25% | 1 | 1 | 0% | 1,436 | 4,842 | +237% | 0 | 0 | — |
case-16 | pass→pass | 8,966 | 5,684 | -37% | 1 | 1 | 0% | 1,661 | 4,871 | +193% | 0 | 0 | — |
case-17 | pass→pass | 10,101 | 5,028 | -50% | 1 | 1 | 0% | 1,733 | 4,620 | +167% | 0 | 0 | — |
case-18 | pass→pass | 6,722 | 4,396 | -35% | 1 | 1 | 0% | 1,205 | 4,633 | +284% | 0 | 0 | — |
case-19 | pass→pass | 8,362 | 4,818 | -42% | 1 | 1 | 0% | 1,388 | 4,608 | +232% | 0 | 0 | — |
case-20 | pass→pass | 7,327 | 5,068 | -31% | 1 | 1 | 0% | 1,387 | 4,856 | +250% | 0 | 0 | — |
case-21 | pass→pass | 3,120 | 1,807 | -42% | 1 | 1 | 0% | 601 | 4,096 | +582% | 0 | 0 | — |
case-22 | pass→pass | 2,635 | 4,027 | +53% | 1 | 1 | 0% | 437 | 4,636 | +961% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.