/install journal-pipeline
Journal Pipeline
Autonomous content machine for UniqueStaysUSA. Combines SEO research, editorial writing, quality enforcement, and Payload CMS publishing into one pipeline. Runs as a PRD-driven loop — pick next article, execute 7 phases, commit, repeat.
Default Behavior: Autonomous Mode
When invoked without arguments, this skill runs automatically:
- Read the content calendar (
KEYWORD_RESEARCH_AND_CONTENT_CALENDAR.md) - Identify the next uncompleted entry
- Execute all 7 phases without stopping
- Only pause if quality gate fails (score below 8.0/10) or user asks
User overrides:
- Specific topic:
/journal-pipeline best cabins near Yellowstone - Research only:
/journal-pipeline --research-only - Draft only:
/journal-pipeline --draft-only - Pause point: "stop after the brief"
If no override is given, assume autonomous mode and execute the full pipeline.
The 7-Phase Pipeline
Each phase maps to a PRD story type. The pipeline reads and updates scripts/ralph/prd.json for sprint state persistence.
Phase 1: STRATEGY (PLAN-xxx)
Research what to write before writing a single word.
Read these files:
KEYWORD_RESEARCH_AND_CONTENT_CALENDAR.md— content pillars, keyword clusters, monthly scheduledocs/uniquestays-gtm-strategy.md— distribution channels, KPIs, content goalsscripts/ralph/progress.txt— what's been done, learned patterns
If no topic specified — auto-select from calendar:
- Parse the current month's table entries
- For each journal post entry, check if a matching published post exists:
GET ${NEXT_PUBLIC_SERVER_URL}/api/blog-posts?where[status][equals]=published&depth=0&limit=50 - Select the first uncompleted entry
- Announce: "Starting autonomous creation of [topic] — next in calendar queue"
- Proceed directly to Phase 2
Keyword research (always run):
- Use WebSearch to check keyword volumes and difficulty for the target keyword
- Search for the top 3 ranking articles for the target keyword — note their angles, gaps, word counts
- Check AI citation patterns — search for the topic on Perplexity/ChatGPT to see what sources get cited
- Identify the competitive gap: what angle is no one covering?
Article type selection:
| Signal | Type | Template |
|---|---|---|
| Topic names a specific city/region | Destination Dispatch | 3-5 stays, 1,400-2,000 words |
| Topic names a stay category | Curated Roundup | 8-12 stays, 1,800-2,500 words |
| Topic names a season/month | Seasonal Guide | 5-8 stays, 1,500-2,200 words |
| Topic names an activity | Activity-Based Guide | 4-7 stays, 1,400-2,000 words |
| Topic focuses on one property | Stay Spotlight | 1 stay, 1,000-1,500 words |
Stay selection:
# For destination dispatches
GET /api/stays?where[state][equals]={State}&where[rating][greater_than_equal]=4.7&limit=20&depth=1&sort=-rating
# For roundups
GET /api/stays?where[category][equals]={categoryId}&limit=50&depth=1&sort=-rating
# For activity-based (search by tags)
GET /api/stays?where[tags.tag][contains]={activity}&limit=20&depth=1
Select stays ensuring:
- Geographic diversity (for roundups, at least 4+ states)
- Quality floor:
rating >= 4.7,reviewCount >= 30 - Valid
affiliateUrl(starts withhttps://) - Hero image exists (
imageUrlorimagerelationship)
Auto-proceed unless: No clear strategic gap exists — only then stop and propose alternatives.
Output: Sprint plan with target keyword, article type, selected stays, competitive angle. Update scripts/ralph/prd.json with 7 stories for this sprint.
Phase 2: RESEARCH (RESEARCH-xxx)
Collect the raw material.
For each selected stay, collect from Payload (depth=1):
title,subtitle,location,state,regionprice,rating,reviewCount,platformdescription,tags(array of tag objects)affiliateUrl,imageUrlsleeps,bedrooms
Identify the "specific detail" for each stay: Find at least one concrete detail that could not apply to any other property. Sources:
- The stay name itself (e.g., "Redwood Treehouse" → built into a specific redwood)
tagsarray (e.g., "Wood-Burning Stove", "Stargazing Deck")descriptionfield (look for named landmarks, distances, species, history)- Geographic context (elevation, nearest town, driving time)
External source research:
- Search for 2-3 authoritative sources to cite (official tourism, NPS, established publications)
- Verify any statistics planned for the article
- Note seasonal information, booking trends, or recent news about the destination
Verify:
- All stays have valid
affiliateUrl - All stays have images
- No two stays have identical descriptions (if they do, note for differentiation in writing)
Output: Stay data collection with specific details identified per stay. Mark RESEARCH as passed in prd.json.
Phase 3: WRITE (WRITE-xxx)
Write the full editorial draft.
Invoke /elite-copywriter with:
- The content brief (topic, keyword, article type, competitive angle)
- The brand voice profile (reference
docs/uniquestays-brand-guidelines.md) - The stay data from Phase 2
- The article type template from
references/article-templates.md
Writing rules:
- Open with the reader's experience, not the property's features
- Lead with feeling, follow with fact — every paragraph
- One idea per paragraph. Short sentences for impact. Long sentences for atmosphere.
- Use "you" for direct address, "we" for editorial observations
- Specific numbers over vague claims ("$285/night" not "affordable")
- Weave price and rating into prose naturally, not as separate callouts
- Include
[EMBED: stay-slug]placeholders where each stay should appear - Never write meta-commentary sections ("Why this matters", "The takeaway")
- No banned words (see
references/quality-checklist.md) - No exclamation marks
File location: content/drafts/{slug}.md
Frontmatter format:
---
title: ""
subtitle: ""
slug: ""
excerpt: ""
city: ""
state: ""
latitude: ""
longitude: ""
metaTitle: ""
metaDescription: ""
publishedAt: ""
status: "draft"
heroImage: "[description or source URL]"
linkedStays:
- stay-slug-1
- stay-slug-2
---
Output: First draft saved. Mark WRITE as passed in prd.json.
Phase 4: SEO (SEO-xxx)
Optimize for search and AI citation. Read references/seo-requirements.md for the full checklist.
Keyword placement:
- Verify primary keyword in: title, metaTitle, first 100 words, at least one H2, metaDescription, excerpt
- Weave 2-3 long-tail variations into body paragraphs naturally
- Check keyword never feels forced
AI citation blocks:
- Add 1-2 definition sentences early in the article (clear, self-contained, extractable)
- Add an FAQ section with 3-5 questions matching common queries
- Verify 2-3 sourced statistics with real attribution
- Add structured comparison table if applicable
Internal linking:
- Query Payload for related posts:
GET /api/blog-posts?where[status][equals]=published&where[state][equals]={state}&depth=0&limit=10 - Add 2+ contextual inline links to other journal posts
- Link to relevant spoke pages where topic overlaps
Meta verification:
metaTitleunder 60 chars, includes keyword, reads like editorialmetaDescriptionunder 160 chars, includes keyword + specific detailslugis kebab-case, no dates in path
Cannibalization check:
- List all published posts from Payload
- Verify no existing post targets the same primary keyword
- If overlap exists, differentiate the angle
Output: SEO-optimized draft. Mark SEO as passed in prd.json.
Phase 5: REVIEW (REVIEW-xxx)
Quality gate. Score against the rubric in references/quality-checklist.md.
Scoring: Rate each of the 8 criteria (voice match, specificity, feeling-first, banned words, SEO, embeds, practical value, cut test) on a 1-10 scale with weighted average.
| Score | Action |
|---|---|
| 8.0+ | AUTO-PROCEED to Phase 6 |
| 7.0-7.9 | One more edit pass targeting failing criteria, then re-score |
| Below 7.0 | STOP — identify what's missing, may need partial rewrite |
Quality scans:
- Banned word scan — zero tolerance. Search for: stunning, breathtaking, magical, life-changing, perfect, amazing, incredible, unforgettable, cozy, hidden gem, wanderlust, bucket list, must-see, nestled, escape the everyday, "whether you're", "perfect for"
- Exclamation mark scan — zero tolerance in journal posts
- 20% cut test — identify the weakest 20% of paragraphs. Cut or rewrite them
- Feeling-first check — verify no stay section opens with features instead of feeling
- Irish Storytelling Test — verify at least one specific detail per stay that could only apply to that place
- Distinct voice check — verify no two stay descriptions sound alike
Save v2 to content/drafts/{slug}-v2.md.
Output: Quality score, specific improvements made. Mark REVIEW as passed in prd.json.
Phase 6: PUBLISH (PUBLISH-xxx)
Publish directly to Payload CMS. No intermediate scripts needed.
Step 1: Resolve stay IDs
# For each stay slug in linkedStays
GET /api/stays?where[slug][equals]={stay-slug}&depth=0&limit=1
# Collect: docs[0].id
Step 2: Upload hero image (if external URL, not already in media collection)
# Fetch image, then upload to Payload media
# Use the two-step pattern from existing scripts
Step 3: Check for existing post
GET /api/blog-posts?where[slug][equals]={slug}&depth=0&limit=1
- If
totalDocs === 0→POST /api/blog-posts - If
totalDocs === 1→PATCH /api/blog-posts/{id}
Step 4: Construct Lexical JSON
Use these helper functions to build the content:
function text(content: string) {
return { type: 'text', format: 0, style: '', mode: 'normal', text: content, detail: 0, version: 1 }
}
function para(content: string) {
return {
type: 'paragraph', format: '', indent: 0, version: 1, direction: 'ltr',
textFormat: 0, textStyle: '',
children: [text(content)],
}
}
function h2(content: string) {
return {
type: 'heading', tag: 'h2', format: '', indent: 0, version: 1, direction: 'ltr',
children: [text(content)],
}
}
function embedBlock(stayId: number) {
return {
type: 'block', version: 2,
fields: { id: crypto.randomUUID(), blockType: 'stayEmbed', stay: stayId },
}
}
function hr() {
return { type: 'horizontalrule', version: 1 }
}
Step 5: Two-step update (follows the pattern in scripts/update-treehouse-article.ts)
First call — update heroImage + linkedStays + editorial fields:
PATCH /api/blog-posts/{id}
{
"title": "...",
"subtitle": "...",
"excerpt": "...",
"heroImage": \x3Cmedia_id>,
"linkedStays": [\x3Cstay_id_1>, \x3Cstay_id_2>, ...],
"city": "...",
"state": "...",
"latitude": "...",
"longitude": "...",
"metaTitle": "...",
"metaDescription": "...",
"status": "published",
"publishedAt": "\x3CISO datetime>"
}
Second call — update content with Lexical JSON:
PATCH /api/blog-posts/{id}
{
"content": { "root": { ... } }
}
Step 6: Verify
# Check the API record
GET /api/blog-posts?where[slug][equals]={slug}&depth=1&limit=1
# Check the public page loads
GET https://uniquestaysusa.com/journal/{slug}
Step 7: Save final version to content/published/{slug}.md
Authentication: Authorization: users API-Key {key} — read from environment, never hardcode.
ISR revalidation: Automatic via Payload's afterChange hook in src/collections/BlogPosts.ts. No manual revalidation needed.
Output: Post live at /journal/{slug}. Mark PUBLISH as passed in prd.json.
Phase 7: SYNC (SYNC-xxx)
Update tracking documents. Mandatory — never skip.
Update scripts/ralph/progress.txt:
Append a sprint summary:
### Sprint {N}: {Article Title}
**PLAN-{N}** ✓ — {keyword}, {article type}, {N} stays selected
**RESEARCH-{N}** ✓ — Stay data collected, {N} specific details identified
**WRITE-{N}** ✓ — First draft: content/drafts/{slug}.md
**SEO-{N}** ✓ — Keyword optimized, {N} internal links, FAQ added
**REVIEW-{N}** ✓ — Quality score: {X}/10, {N} edits
**PUBLISH-{N}** ✓ — content/published/{slug}.md
- Published at: /journal/{slug}
- Target keyword: {keyword}
- Word count: {N}
- Quality score: {X}/10
Record learned patterns in the progress file (what worked, what to do differently next sprint).
Verify sitemap inclusion:
GET https://uniquestaysusa.com/sitemap.xml
# Check that /journal/{slug} appears
Update scripts/ralph/prd.json:
- All sprint stories set to
passes: true - Increment
sprintNumber - Clear
currentStoryfor next sprint
Git commit:
git add content/published/{slug}.md scripts/ralph/prd.json scripts/ralph/progress.txt
git commit -m "journal: publish \"{title}\" (sprint {N})"
Output: All tracking docs updated and committed. Mark SYNC as passed. Loop continues to next sprint.
Loop Control
State persistence
The loop state lives in three files:
| File | Purpose |
|---|---|
scripts/ralph/prd.json |
Sprint stories, pass/fail status, sprint number |
scripts/ralph/progress.txt |
Running log of completed work and learned patterns |
.claude/ralph-loop.local.md |
Ralph stop hook state (active, iteration count, completion promise) |
Start of each iteration
- Read
.claude/ralph-loop.local.md— is a Ralph loop active? - Read
scripts/ralph/prd.json— what's the current sprint and story? - Read
scripts/ralph/progress.txt— what patterns have been learned? - Find the highest-priority story where
passes: false - Execute that story's phase
- Update
prd.jsonwithpasses: true - If all stories pass, start a new sprint (return to Phase 1)
Stop conditions
- All calendar entries for the current month are completed
- Quality gate fails 3 times on the same article (escalate to user)
- User explicitly cancels (
/ralph-cancelor removesactive: truefrom loop state) - User says "stop", "pause", or "that's enough for now"
Ralph stop hook integration
The existing Ralph stop hook at .claude/ralph-loop.local.md controls loop persistence across context windows. The skill reads this file at the start of each iteration. When a context window closes, the stop hook re-feeds the journal-pipeline prompt to continue where it left off.
Content Calendar Integration
Reading the calendar
The calendar lives at KEYWORD_RESEARCH_AND_CONTENT_CALENDAR.md. Parse the monthly tables:
| Column | Use |
|---|---|
| Week | Scheduling |
| Content Type | Journal Post vs Lead Magnet vs Programmatic |
| Title/Topic | The article topic |
| Target Keywords | Primary + secondary keywords |
| Goal | The strategic purpose |
Auto-select logic
- Find the current month's section
- Filter for "Journal Post" content type entries
- For each entry, check if a published post exists in Payload with matching topic
- Select the first entry with no published match
- If the current month has no remaining entries, advance to the next month
- If no entries remain, report "calendar complete" and suggest planning next quarter
Article type inference
| Calendar signal | Article type |
|---|---|
| Topic mentions a city/state/region | Destination Dispatch |
| Topic mentions a stay category (treehouses, cabins, domes) | Curated Roundup |
| Topic mentions a season or month | Seasonal Guide |
| Topic mentions an activity (stargazing, fishing, hiking) | Activity-Based Guide |
| Topic mentions a specific property by name | Stay Spotlight |
When NOT to Use This Skill
- Updating existing published posts (use
elite-copywriterfor polish passes) - Creating listing pages (that's programmatic SEO, a separate system)
- Social media content (future extension)
- Newsletter content (future extension)
- Technical documentation or code changes
- Make sure OpenClaw is installed (local or Docker)
- Run the install command in chat:
/install journal-pipeline - After installation, invoke the skill by name or use
/journal-pipeline - Provide required inputs per the skill's parameter spec and get structured output
What is Journal Pipeline?
Autonomous journal content pipeline for UniqueStaysUSA. Researches keywords, writes editorial content, publishes to Payload CMS, and tracks results. Use when... It is an AI Agent Skill for Claude Code / OpenClaw, with 44 downloads so far.
How do I install Journal Pipeline?
Run "/install journal-pipeline" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.
Is Journal Pipeline free?
Yes, Journal Pipeline is completely free, licensed under MIT-0. You can download, install and use it at no cost.
Which platforms does Journal Pipeline support?
Journal Pipeline is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).
Who created Journal Pipeline?
It is built and maintained by Jon High (@jhigh1594); the current version is v1.0.0.