newsletter-campaign-workflow
Newsletter Campaign Workflow Skill
Purpose
Comprehensive guide for working with the AIProDaily newsletter platform's campaign workflow system, including RSS processing, article generation, multi-tenant data management, and automated publication.
When to Use
Automatically activates when working with:
- Campaign creation and management
- RSS feed processing and article generation
- Workflow steps and automation
- Newsletter publication and sending
- MailerLite integration
- Multi-tenant campaign operations
- Advertorial and ad management
- Campaign status transitions
System Architecture
Multi-Tenant Structure
Newsletter (slug: "accounting")
→ publication_id (UUID)
→ Campaigns (daily)
→ RSS Posts (scored, assigned)
→ Articles (generated from posts)
→ Email (sent via MailerLite)
CRITICAL: ALL database queries MUST filter by publication_id
Issue Status Lifecycle
draft → processing → ready → approved → sent
↓ (if error)
failed
Status Meanings:
draft: Issue created, ready for workflowprocessing: Workflow actively runningready: Content generated, ready for reviewapproved: Manual approval for sendingsent: Published to subscribersfailed: Workflow error occurred
Note: "Issue" replaced "campaign" in the codebase. The issues table was formerly newsletter_campaigns.
Core Workflow: 10-Step RSS Processing
Location: src/lib/workflows/process-rss-workflow.ts
Architecture: Vercel Workflows
Timeout: 800 seconds per step
Trigger: /api/cron/trigger-workflow (every 5 minutes)
Workflow Steps
-
Setup (800s)
- Create tomorrow's campaign
- Select AI apps/prompts
- Assign top 24 posts (12 primary + 12 secondary)
- Run deduplication
-
Generate Primary Titles (800s)
- Generate 6 primary headlines
3-4. Generate Primary Bodies (800s each)
- Batch 1: Generate 3 primary articles
- Batch 2: Generate 3 more primary articles
-
Fact-Check Primary (800s)
- Fact-check all 6 primary articles
- Store fact_check_score (0-10)
-
Generate Secondary Titles (800s)
- Generate 6 secondary headlines
7-8. Generate Secondary Bodies (800s each)
- Batch 1: Generate 3 secondary articles
- Batch 2: Generate 3 more secondary articles
-
Fact-Check Secondary (800s)
- Fact-check all 6 secondary articles
-
Finalize (800s)
- Auto-select top 3 per section
- Generate welcome section
- Generate subject line
- Set status to
draft - Unassign unused posts
Workflow Best Practices
✅ Error Handling Pattern:
let retryCount = 0
const maxRetries = 2
while (retryCount <= maxRetries) {
try {
await processStep()
return // Success
} catch (error) {
retryCount++
if (retryCount > maxRetries) {
console.error('[Step X/10] Failed after retries')
throw error
}
console.log(`[Step X/10] Retrying (${retryCount}/${maxRetries})...`)
await new Promise(resolve => setTimeout(resolve, 2000))
}
}
✅ Logging Pattern:
// One-line summaries with prefixes
console.log('[Workflow] Step 1/10: Setup complete, 24 posts assigned')
console.log('[AI] Batch 1/4: Scored 3 posts, avg: 7.2')
console.error('[DB] Query failed:', error.message)
Log Prefixes:
[Workflow]- Vercel Workflow orchestration[RSS]- RSS processing[AI]- OpenAI/Claude API calls[DB]- Database operations[CRON]- Cron job execution
Critical Rules
1. Multi-Tenant Isolation
ALWAYS filter by publication_id:
// ✅ CORRECT
const { data } = await supabaseAdmin
.from('articles')
.select('*')
.eq('campaign_id', campaignId)
.eq('publication_id', newsletterId) // REQUIRED
// ❌ WRONG - Data leakage!
const { data } = await supabaseAdmin
.from('articles')
.select('*')
.eq('campaign_id', campaignId)
2. Date/Time Handling
NEVER use UTC conversions for date comparisons:
// ✅ CORRECT: Local date comparison
const dateStr = date.split('T')[0] // "2025-01-07"
const today = new Date().toISOString().split('T')[0]
if (dateStr === today) { /* ... */ }
// ❌ FORBIDDEN: UTC conversion shifts dates
date.toISOString() // Wrong timezone!
date.toUTCString() // Breaks comparisons!
Why: UTC conversion shifts dates by timezone. Users expect Central Time.
3. Performance & Limits
Hard Limits (Vercel):
- Workflow step timeout: 800 seconds (13 minutes per step)
- API route timeout: 600 seconds (10 minutes max)
- Log size: 10MB maximum
- Memory: 1024MB default
AI Integration
Standard Pattern: callAIWithPrompt()
Location: src/lib/openai.ts
import { callAIWithPrompt } from '@/lib/openai'
const result = await callAIWithPrompt(
'ai_prompt_primary_article_title', // Key in app_settings
newsletterId,
{
title: post.title,
description: post.description,
content: post.full_article_text
}
)
// result = { headline: "Your Generated Title" }
How it works:
- Loads complete JSON prompt from
app_settingstable - Replaces placeholders (e.g.,
{{title}},{{content}}) - Calls AI API (OpenAI or Claude)
- Returns parsed JSON response
Prompt Storage Format
INSERT INTO app_settings (key, value, publication_id, ai_provider)
VALUES (
'ai_prompt_primary_article_title',
'{
"model": "gpt-4o",
"temperature": 0.7,
"max_output_tokens": 500,
"response_format": { "type": "json_schema", "json_schema": {...} },
"messages": [
{"role": "system", "content": "You are a headline writer..."},
{"role": "user", "content": "Title: {{title}}\n\nWrite a headline."}
]
}',
'newsletter-uuid',
'openai'
);
All parameters stored in database, not hardcoded.
Database Schema (Key Tables)
Note: "Issues" replaced "campaigns" in the database. The table issues was formerly newsletter_campaigns.
publications (formerly newsletters)
├── issues (status: draft → processing → ready → sent)
│ ├── issue_articles (primary section, 6 generated, 3 active)
│ ├── secondary_articles (secondary section, 6 generated, 3 active)
│ └── rss_posts (assigned posts)
│ └── post_ratings (multi-criteria scores)
│
├── rss_feeds (active/inactive, section assignment)
├── publication_settings (key-value config, scoped by publication_id)
├── advertisements (advertorials for rotation)
├── issue_advertisements (tracks ad usage per issue)
└── archived_articles, archived_rss_posts (historical data)
API Route Template
// app/api/[feature]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
if (!body.campaignId) {
return NextResponse.json(
{ error: 'Missing campaignId' },
{ status: 400 }
)
}
const result = await processData(body)
return NextResponse.json({ data: result })
} catch (error: any) {
console.error('[API] Error:', error.message)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export const maxDuration = 600 // 10 minutes for long operations
Common Tasks
Create New Campaign
const { data: campaign } = await supabaseAdmin
.from('newsletter_campaigns')
.insert({
publication_id: newsletterId,
date: tomorrowDate, // YYYY-MM-DD format
status: 'draft',
subject_line: null
})
.select()
.single()
Assign RSS Posts to Campaign
await supabaseAdmin
.from('rss_posts')
.update({
campaign_id: campaignId,
assigned_at: new Date().toISOString(),
section: 'primary' // or 'secondary'
})
.in('id', topPostIds)
.eq('publication_id', newsletterId) // REQUIRED
Generate Article Content
const result = await callAIWithPrompt(
'ai_prompt_primary_article_body',
newsletterId,
{
title: post.title,
description: post.description,
content: post.full_article_text
}
)
await supabaseAdmin
.from('articles')
.insert({
campaign_id: campaignId,
publication_id: newsletterId, // REQUIRED
rss_post_id: post.id,
headline: result.headline,
article_text: result.body,
fact_check_score: null,
is_active: false
})
Automation & Cron Jobs
Configuration: vercel.json
Active Crons
| Cron | Schedule | Purpose |
|---|---|---|
/api/cron/trigger-workflow |
Every 5 min | Trigger RSS workflow if scheduled |
/api/cron/ingest-rss |
Every 15 min | Fetch & score new RSS posts |
/api/cron/send-review |
Every 5 min | Create MailerLite campaign and send review email |
/api/cron/send-final |
Every 5 min | Send final issues (status: approved) |
/api/cron/send-secondary |
Every 5 min | Send secondary newsletter |
/api/cron/monitor-workflows |
Every 5 min | Check for failed/stuck workflows |
/api/cron/process-mailerlite-updates |
Every 5 min | Process MailerLite webhooks |
/api/cron/cleanup-pending-submissions |
Daily 7 AM | Clear stale ad submissions |
/api/cron/import-metrics |
Daily 6 AM | Sync MailerLite metrics |
/api/cron/health-check |
Every 5 min (8AM-10PM) | System health check |
Not Implemented (registered in vercel.json but empty)
| Cron | Schedule | Notes |
|---|---|---|
/api/cron/populate-events |
Every 5 min | Events system not implemented |
/api/cron/sync-events |
Daily midnight | Events system not implemented |
/api/cron/generate-weather |
Daily 8 PM | Route file missing |
/api/cron/collect-wordle |
Daily 7 PM | Route file missing |
Troubleshooting
Campaign Stuck in "processing"
-- Check workflow status
SELECT id, status, date, created_at, updated_at
FROM newsletter_campaigns
WHERE status = 'processing'
AND publication_id = 'your-newsletter-id'
ORDER BY created_at DESC;
-- Reset to draft (if needed)
UPDATE newsletter_campaigns
SET status = 'draft'
WHERE id = 'campaign-id'
AND publication_id = 'your-newsletter-id';
Posts Not Scoring
- Check RSS ingestion:
/api/cron/ingest-rsslogs - Verify criteria config:
SELECT * FROM app_settings WHERE key LIKE 'criteria_%' AND publication_id = ? - Check prompts exist:
SELECT * FROM app_settings WHERE key LIKE 'ai_prompt_criteria_%' AND publication_id = ? - Verify feeds active:
SELECT * FROM rss_feeds WHERE active = true AND publication_id = ?
Workflow Failures
- Check Vercel logs:
vercel logs --since 1h - Check workflow monitor cron:
/api/cron/monitor-workflows - Look for timeout errors (step > 800s)
- Check retry count in logs
Reference Documentation
See claude.md in project root for:
- Complete workflow details
- Multi-criteria scoring system
- RSS feed management
- MailerLite integration
- Advertorial rotation
- Section management
Skill Status: ACTIVE ✅ Line Count: < 500 (following best practices) ✅ Project-Specific: Tailored for AIProDaily tech stack ✅
More from venture-formations/aiprodaily
nextjs-api-routes
Next.js 15 API route patterns, NextRequest, NextResponse, error handling, maxDuration configuration, authentication, request validation, server-side operations, route handlers, and API endpoint best practices. Use when creating API routes, handling requests, configuring timeouts, or building server-side endpoints.
47ai-content-generation
AI content generation with OpenAI and Claude, callAIWithPrompt usage, prompt storage in app_settings, structured outputs, response format validation, multi-criteria scoring, rate limiting, JSON schema, and AI API best practices. Use when generating content, creating prompts, scoring articles, or working with OpenAI/Claude APIs.
33supabase-database-ops
Critical guardrail for Supabase database operations ensuring multi-tenant isolation with publication_id filtering, proper use of supabaseAdmin, avoiding SELECT *, error handling patterns, and secure server-side database access. Use when writing database queries, working with supabase, accessing newsletter_campaigns, articles, rss_posts, or any tenant-scoped data.
30skill-developer
Create and manage Claude Code skills following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns, file paths, content patterns), enforcement levels (block, suggest, warn), hook mechanisms (UserPromptSubmit, PreToolUse), session tracking, and the 500-line rule.
24