payload
This skill should be used when the user asks to "create a post", "edit a post", "update post content", "list posts", "convert markdown to Lexical", "write article to Payload", or mentions Payload CMS content management. Handles both production and local Payload sites via REST API with authentication.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install b-open-io-prompts-payload
Repository
Skill path: skills/payload
This skill should be used when the user asks to "create a post", "edit a post", "update post content", "list posts", "convert markdown to Lexical", "write article to Payload", or mentions Payload CMS content management. Handles both production and local Payload sites via REST API with authentication.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Backend, Tech Writer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: b-open-io.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install payload into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/b-open-io/prompts before adding payload to shared team environments
- Use payload for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: payload
description: This skill should be used when the user asks to "create a post", "edit a post", "update post content", "list posts", "convert markdown to Lexical", "write article to Payload", or mentions Payload CMS content management. Handles both production and local Payload sites via REST API with authentication.
version: 0.4.0
---
# Payload CMS Operations
Manage Payload CMS content via REST API. Works with any Payload deployment (production or local).
## When to Use
- Creating or editing posts/pages in Payload CMS
- Converting markdown content to Lexical rich text format
- Listing and querying Payload collections
- Bulk content updates
## Workflow: REST API with Authentication
### Step 1: Determine the API Endpoint
Ask the user for their Payload site URL, or check common locations:
```bash
# Production site (ask user or check project config)
curl -s "https://your-site.com/api/posts?limit=1" | head -c 100
# Local development
curl -s "http://localhost:3000/api/posts?limit=1" 2>/dev/null | head -c 100
curl -s "http://localhost:3010/api/posts?limit=1" 2>/dev/null | head -c 100
```
### Step 2: Authenticate
For mutations (create/update/delete), authentication is required. Payload uses session-based auth.
**Option A: User provides credentials**
```bash
# Login to get auth token
curl -X POST "https://your-site.com/api/users/login" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "..."}' \
-c cookies.txt
# Use the cookie file for authenticated requests
curl -X POST "https://your-site.com/api/posts" \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"title": "...", "content": {...}}'
```
**Option B: User logs in via admin UI**
Have the user log in at `/admin`, then extract the `payload-token` cookie from their browser for use in API calls.
### Step 3: Create/Update Content
```bash
# Create a post
curl -X POST "https://your-site.com/api/posts" \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"title": "Post Title",
"slug": "post-slug",
"content": { "root": { ... } },
"_status": "published"
}'
# Update a post
curl -X PATCH "https://your-site.com/api/posts/POST_ID" \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"content": { "root": { ... } }}'
```
### Step 4: Verify
```bash
# Check the post was created
curl -s "https://your-site.com/api/posts?where[slug][equals]=post-slug" | jq '.docs[0]'
```
## Lexical JSON Structure
Payload's Lexical editor stores content as JSON:
```json
{
"root": {
"type": "root",
"format": "",
"indent": 0,
"version": 1,
"direction": "ltr",
"children": [
{
"type": "paragraph",
"format": "",
"indent": 0,
"version": 1,
"direction": "ltr",
"children": [
{"type": "text", "text": "Content here", "mode": "normal", "format": 0, "detail": 0, "version": 1, "style": ""}
]
}
]
}
}
```
### Supported Node Types
| Markdown | Lexical Node |
|----------|--------------|
| Paragraphs | `paragraph` |
| `# Heading` | `heading` with tag h1-h6 |
| `**bold**` | text with format: 1 |
| `*italic*` | text with format: 2 |
| `` `code` `` | text with format: 16 |
| Code blocks | `block` with blockType: "code" |
| Lists | `list` with `listitem` children |
| `> quotes` | `quote` |
### Text Format Bitmask
| Value | Format |
|-------|--------|
| 0 | Normal |
| 1 | Bold |
| 2 | Italic |
| 3 | Bold + Italic |
| 16 | Code |
## Markdown to Lexical Conversion
The skill includes a Python script for converting markdown to Lexical JSON:
```bash
python3 ${SKILL_DIR}/scripts/md_to_lexical.py article.md > /tmp/content.json
```
## Common Collections
| Collection | Slug | Purpose |
|------------|------|---------|
| Posts | `posts` | Blog posts |
| Pages | `pages` | Static pages |
| Media | `media` | Uploaded files |
| Users | `users` | User accounts |
## Local Development Alternative
If working locally and REST auth is problematic, write an inline script in the project:
```typescript
// scripts/create-post.ts
import { getPayload } from 'payload'
import config from '../src/payload.config'
const payload = await getPayload({ config })
await payload.create({
collection: 'posts',
data: { title: '...', content: {...}, _status: 'published' }
})
process.exit(0)
```
Run with: `source .env.local && bunx tsx scripts/create-post.ts`
**Note**: If Drizzle prompts for schema migration, answer 'n' and use REST API instead.
## Additional Resources
- **`references/lexical-format.md`** - Complete Lexical node type reference
- **`references/rest-api.md`** - Full REST API documentation
- **`scripts/md_to_lexical.py`** - Markdown to Lexical converter
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/create-post.ts
```typescript
/**
* Create a Payload post using the Local API
*
* Usage: PAYLOAD_SECRET="..." DATABASE_URI="..." bunx tsx scripts/create-post.ts <title> <slug> <content.json>
*
* This script uses Payload's Local API which bypasses REST authentication.
* Run from within the Payload project directory with proper environment variables.
*/
import { getPayload } from "payload";
import configPromise from "@payload-config";
import { readFileSync } from "node:fs";
async function main() {
const [title, slug, contentPath] = process.argv.slice(2);
if (!title || !slug || !contentPath) {
console.error("Usage: bunx tsx create-post.ts <title> <slug> <content.json>");
console.error(" title: Post title");
console.error(" slug: URL slug for the post");
console.error(" content.json: Path to Lexical JSON content file");
process.exit(1);
}
const payload = await getPayload({ config: configPromise });
// Check if slug already exists
const existing = await payload.find({
collection: "posts",
where: { slug: { equals: slug } },
limit: 1,
});
if (existing.docs.length > 0) {
console.error(`Post with slug "${slug}" already exists (ID: ${existing.docs[0].id})`);
process.exit(1);
}
// Read and parse content file
const contentJson = readFileSync(contentPath, "utf-8");
const content = JSON.parse(contentJson);
// Create the post
const post = await payload.create({
collection: "posts",
data: {
title,
slug,
content,
_status: "published",
},
});
console.log(`Created post: ${post.title}`);
console.log(`ID: ${post.id}`);
console.log(`URL: ${process.env.NEXT_PUBLIC_SERVER_URL}/posts/${post.slug}`);
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});
```
### references/lexical-format.md
```markdown
# Lexical JSON Format Reference
Complete reference for Payload CMS Lexical editor node types.
## Root Structure
Every Lexical document has this structure:
```json
{
"root": {
"type": "root",
"format": "",
"indent": 0,
"version": 1,
"children": [ /* nodes */ ],
"direction": "ltr"
}
}
```
## Text Formatting
Text format is a bitmask integer:
| Value | Format |
|-------|--------|
| 0 | Normal |
| 1 | Bold |
| 2 | Italic |
| 3 | Bold + Italic |
| 4 | Strikethrough |
| 8 | Underline |
| 16 | Code |
| 32 | Subscript |
| 64 | Superscript |
Combine values: Bold + Italic = 1 + 2 = 3
## Node Types
### Text Node
```json
{
"type": "text",
"text": "Content here",
"format": 0,
"style": "",
"detail": 0,
"mode": "normal",
"version": 1
}
```
### Paragraph Node
```json
{
"type": "paragraph",
"format": "",
"indent": 0,
"version": 1,
"children": [
{ "type": "text", "text": "...", "format": 0, "version": 1 }
],
"direction": "ltr",
"textFormat": 0
}
```
### Heading Node
```json
{
"type": "heading",
"tag": "h2",
"format": "",
"indent": 0,
"version": 1,
"children": [
{ "type": "text", "text": "Heading Text", "format": 0, "version": 1 }
],
"direction": "ltr"
}
```
Valid tags: h1, h2, h3, h4, h5, h6
### Link Node
```json
{
"type": "link",
"format": "",
"indent": 0,
"version": 1,
"children": [
{ "type": "text", "text": "Link Text", "format": 0, "version": 1 }
],
"direction": "ltr",
"fields": {
"url": "https://example.com",
"newTab": false,
"linkType": "custom"
}
}
```
### List Node
```json
{
"type": "list",
"listType": "bullet",
"format": "",
"indent": 0,
"version": 1,
"start": 1,
"tag": "ul",
"children": [
{
"type": "listitem",
"format": "",
"indent": 0,
"version": 1,
"value": 1,
"children": [
{ "type": "text", "text": "Item text", "format": 0, "version": 1 }
],
"direction": "ltr"
}
],
"direction": "ltr"
}
```
List types:
- `"bullet"` with tag `"ul"` for unordered
- `"number"` with tag `"ol"` for ordered
- `"check"` for checkbox lists
### Quote Node
```json
{
"type": "quote",
"format": "",
"indent": 0,
"version": 1,
"children": [
{ "type": "text", "text": "Quoted text", "format": 0, "version": 1 }
],
"direction": "ltr"
}
```
### Horizontal Rule
```json
{
"type": "horizontalrule",
"version": 1
}
```
### Code Block (Payload Block)
Payload uses block nodes for code:
```json
{
"type": "block",
"format": "",
"indent": 0,
"version": 2,
"fields": {
"id": "",
"blockName": "",
"blockType": "code",
"code": "const x = 1;\nconsole.log(x);",
"language": "typescript"
}
}
```
Common languages: javascript, typescript, python, bash, json, html, css, go, rust
### Media Block
```json
{
"type": "block",
"format": "",
"indent": 0,
"version": 2,
"fields": {
"id": "",
"blockName": "",
"blockType": "mediaBlock",
"media": "media-id-here",
"position": "default"
}
}
```
### Banner Block
```json
{
"type": "block",
"format": "",
"indent": 0,
"version": 2,
"fields": {
"id": "",
"blockName": "",
"blockType": "banner",
"style": "info",
"content": {
"root": {
"type": "root",
"children": [
{
"type": "paragraph",
"children": [
{ "type": "text", "text": "Banner content" }
]
}
]
}
}
}
}
```
## Direction Values
- `"ltr"` - Left to right (default)
- `"rtl"` - Right to left
## Complete Example
A document with heading, paragraph, and code:
```json
{
"root": {
"type": "root",
"format": "",
"indent": 0,
"version": 1,
"children": [
{
"type": "heading",
"tag": "h1",
"format": "",
"indent": 0,
"version": 1,
"children": [
{ "type": "text", "text": "Getting Started", "format": 0, "version": 1 }
],
"direction": "ltr"
},
{
"type": "paragraph",
"format": "",
"indent": 0,
"version": 1,
"children": [
{ "type": "text", "text": "This is an introduction.", "format": 0, "version": 1 }
],
"direction": "ltr",
"textFormat": 0
},
{
"type": "block",
"format": "",
"indent": 0,
"version": 2,
"fields": {
"id": "",
"blockName": "",
"blockType": "code",
"code": "npm install payload",
"language": "bash"
}
}
],
"direction": "ltr"
}
}
```
## Validation
When sending to Payload API:
1. Ensure all nodes have `version` field
2. Root must have `type: "root"` and `children` array
3. Block nodes need `fields.blockType` matching collection config
4. Text nodes inside containers (paragraph, heading, list item, etc.)
## INVALID Node Types (Cause Error #17)
These types will crash the frontend with "Minified Lexical error #17":
| Invalid Type | Why | Use Instead |
|--------------|-----|-------------|
| `code` (direct) | Not registered as top-level | `block` with `blockType: "code"` |
| `code-highlight` | Internal Lexical type | Text with `format: 16` |
| `code-line` | Internal type | Regular text nodes |
| Custom nodes | Not registered | Standard nodes only |
### What Causes Error #17
Lexical error #17 means the editor encountered a node type that isn't registered. This happens when:
1. **Direct database edits** use invalid node structures
2. **Draft versions** contain corrupted content from failed edits
3. **Import/migration** scripts produce non-standard JSON
### Fixing Error #17 in Admin Panel
If the admin panel shows error #17 when editing a document:
```sql
-- Find corrupted draft versions
SELECT id, version__status
FROM _posts_v
WHERE parent_id = POST_ID
AND version_content::text LIKE '%code-highlight%';
-- Delete them
DELETE FROM _posts_v WHERE id IN (corrupted_ids);
```
The admin loads the latest draft version, so corrupted drafts must be removed.
### Safe Direct Database Updates
When updating content directly:
```sql
-- 1. Update the main record with valid Lexical JSON
UPDATE posts
SET content = '{"root": {...}}', updated_at = NOW()
WHERE slug = 'my-post';
-- 2. Delete any draft versions that might have old content
DELETE FROM _posts_v
WHERE parent_id = (SELECT id FROM posts WHERE slug = 'my-post')
AND version__status = 'draft';
```
Then trigger ISR revalidation via Payload API or by saving in admin.
```
### references/rest-api.md
```markdown
# Payload REST API Reference
For external access when Local API is not available.
## Authentication Options
### Session Token
Obtain via login, use in cookie:
```bash
curl -b "payload-token=YOUR_TOKEN" https://example.com/api/posts
```
### Payload Built-in API Keys
If enabled (`useAPIKey: true` on auth collection):
```bash
curl -H "Authorization: users API-Key YOUR_KEY" https://example.com/api/posts
```
**Note**: Many Payload sites use `payload-auth` or `better-auth` which may disable built-in API keys. Check the site's auth configuration.
## Endpoints
### List Documents
```bash
GET /api/{collection}?limit=100
```
### Get by ID
```bash
GET /api/{collection}/{id}
```
### Query by Field
```bash
GET /api/{collection}?where[field][equals]=value
```
### Create Document
```bash
POST /api/{collection}
Content-Type: application/json
{
"field": "value",
"_status": "published"
}
```
### Update Document
```bash
PATCH /api/{collection}/{id}
Content-Type: application/json
{
"field": "new value"
}
```
### Delete Document
```bash
DELETE /api/{collection}/{id}
```
## Query Operators
| Operator | Example |
|----------|---------|
| equals | `where[status][equals]=published` |
| not_equals | `where[status][not_equals]=draft` |
| like | `where[title][like]=search` |
| contains | `where[tags][contains]=tech` |
| in | `where[category][in]=1,2,3` |
| greater_than | `where[views][greater_than]=100` |
| less_than | `where[views][less_than]=1000` |
## Pagination
```bash
GET /api/posts?limit=10&page=2
```
Response includes:
- `docs`: Array of documents
- `totalDocs`: Total count
- `totalPages`: Number of pages
- `page`: Current page
- `hasNextPage`: Boolean
- `hasPrevPage`: Boolean
## Error Codes
| Code | Meaning |
|------|---------|
| 400 | Invalid JSON or missing required fields |
| 401 | Authentication required |
| 403 | Insufficient permissions |
| 404 | Collection or document not found |
| 500 | Server error |
```
### scripts/md_to_lexical.py
```python
#!/usr/bin/env python3
"""
Convert Markdown to Payload CMS Lexical JSON format.
Usage:
python3 md_to_lexical.py input.md
python3 md_to_lexical.py input.md > output.json
cat input.md | python3 md_to_lexical.py
"""
import sys
import json
import re
def text_node(text: str, format: int = 0) -> dict:
"""Create a text node. Format: 0=normal, 1=bold, 2=italic, 3=bold+italic"""
return {
"type": "text",
"text": text,
"format": format,
"style": "",
"detail": 0,
"mode": "normal",
"version": 1
}
def paragraph_node(children: list) -> dict:
"""Create a paragraph node."""
return {
"type": "paragraph",
"format": "",
"indent": 0,
"version": 1,
"children": children,
"direction": "ltr",
"textFormat": 0
}
def heading_node(text: str, level: int) -> dict:
"""Create a heading node (h1-h6)."""
return {
"type": "heading",
"tag": f"h{level}",
"format": "",
"indent": 0,
"version": 1,
"children": [text_node(text)],
"direction": "ltr"
}
def code_block_node(code: str, language: str = "") -> dict:
"""Create a code block node."""
return {
"type": "block",
"format": "",
"indent": 0,
"version": 2,
"fields": {
"id": "",
"blockName": "",
"blockType": "code",
"code": code,
"language": language or "plaintext"
}
}
def list_item_node(children: list) -> dict:
"""Create a list item node."""
return {
"type": "listitem",
"format": "",
"indent": 0,
"version": 1,
"value": 1,
"children": children,
"direction": "ltr"
}
def list_node(items: list, ordered: bool = False) -> dict:
"""Create a list node (ul or ol)."""
return {
"type": "list",
"listType": "number" if ordered else "bullet",
"format": "",
"indent": 0,
"version": 1,
"start": 1,
"tag": "ol" if ordered else "ul",
"children": items,
"direction": "ltr"
}
def horizontal_rule_node() -> dict:
"""Create a horizontal rule node."""
return {
"type": "horizontalrule",
"version": 1
}
def parse_inline(text: str) -> list:
"""Parse inline formatting (bold, italic, code, links)."""
children = []
# Pattern for inline code, bold, italic
# Process in order: code first (to avoid conflicts), then bold, then italic
patterns = [
(r'`([^`]+)`', lambda m: text_node(m.group(1), 16)), # code format
(r'\*\*([^*]+)\*\*', lambda m: text_node(m.group(1), 1)), # bold
(r'__([^_]+)__', lambda m: text_node(m.group(1), 1)), # bold alt
(r'\*([^*]+)\*', lambda m: text_node(m.group(1), 2)), # italic
(r'_([^_]+)_', lambda m: text_node(m.group(1), 2)), # italic alt
]
# Simple approach: just return plain text for now
# Complex inline parsing would require more sophisticated tokenization
if text.strip():
# Handle basic bold/italic
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) # strip bold markers
text = re.sub(r'__([^_]+)__', r'\1', text)
text = re.sub(r'\*([^*]+)\*', r'\1', text)
text = re.sub(r'_([^_]+)_', r'\1', text)
text = re.sub(r'`([^`]+)`', r'\1', text)
children.append(text_node(text))
return children
def parse_markdown(md: str) -> dict:
"""Parse markdown and return Lexical JSON structure."""
lines = md.split('\n')
children = []
i = 0
while i < len(lines):
line = lines[i]
# Empty line - skip
if not line.strip():
i += 1
continue
# Heading
heading_match = re.match(r'^(#{1,6})\s+(.+)$', line)
if heading_match:
level = len(heading_match.group(1))
text = heading_match.group(2)
children.append(heading_node(text, level))
i += 1
continue
# Code block
if line.startswith('```'):
language = line[3:].strip()
code_lines = []
i += 1
while i < len(lines) and not lines[i].startswith('```'):
code_lines.append(lines[i])
i += 1
code = '\n'.join(code_lines)
children.append(code_block_node(code, language))
i += 1 # skip closing ```
continue
# Horizontal rule
if re.match(r'^(-{3,}|\*{3,}|_{3,})$', line.strip()):
children.append(horizontal_rule_node())
i += 1
continue
# Unordered list
if re.match(r'^[-*+]\s+', line):
items = []
while i < len(lines) and re.match(r'^[-*+]\s+', lines[i]):
item_text = re.sub(r'^[-*+]\s+', '', lines[i])
items.append(list_item_node(parse_inline(item_text)))
i += 1
children.append(list_node(items, ordered=False))
continue
# Ordered list
if re.match(r'^\d+\.\s+', line):
items = []
while i < len(lines) and re.match(r'^\d+\.\s+', lines[i]):
item_text = re.sub(r'^\d+\.\s+', '', lines[i])
items.append(list_item_node(parse_inline(item_text)))
i += 1
children.append(list_node(items, ordered=True))
continue
# Regular paragraph - collect until empty line or special block
para_lines = []
while i < len(lines):
current = lines[i]
# Stop at empty line, heading, code block, list, hr
if not current.strip():
break
if re.match(r'^#{1,6}\s+', current):
break
if current.startswith('```'):
break
if re.match(r'^[-*+]\s+', current):
break
if re.match(r'^\d+\.\s+', current):
break
if re.match(r'^(-{3,}|\*{3,}|_{3,})$', current.strip()):
break
para_lines.append(current)
i += 1
if para_lines:
para_text = ' '.join(para_lines)
inline_children = parse_inline(para_text)
if inline_children:
children.append(paragraph_node(inline_children))
return {
"root": {
"type": "root",
"format": "",
"indent": 0,
"version": 1,
"children": children,
"direction": "ltr"
}
}
def main():
# Read from file argument or stdin
if len(sys.argv) > 1:
with open(sys.argv[1], 'r') as f:
md = f.read()
else:
md = sys.stdin.read()
lexical = parse_markdown(md)
print(json.dumps(lexical, indent=2))
if __name__ == '__main__':
main()
```