| name | add-cron-endpoint |
| description | Use when creating a new scheduled/cron task in the ExpxBlog project. Enforces the project's cron pattern: pg_cron + pg_net on Supabase (never vercel.json cron), Bearer auth with SUPABASE_SERVICE_ROLE_KEY, maxDuration 800, and logging to automation_logs. Prevents the most common LLM mistake: using Vercel's native cron instead of the project's Supabase-based cron.
|
Skill: Add Cron Endpoint
When to use
Trigger this skill when any of the following is true:
- Creating a new scheduled/periodic task
- The prompt mentions "cron", "agendamento", "scheduled task", "job periódico", "executar automaticamente a cada X horas"
- You are about to add a
crons entry to vercel.json — STOP, that is wrong for this project
When NOT to use
- Modifying the logic inside an existing cron endpoint (authentication and structure are already in place)
- One-off background jobs not triggered by a schedule
- Admin-triggered manual runs (those use
/api/admin/*/run routes, not cron routes)
The Core Rule
Crons in this project run as pg_cron jobs in Supabase that call the Next.js API via pg_net HTTP requests.
Never add crons to vercel.json. Never use Vercel's native cron scheduling. The only correct pattern is:
- Create the API route in
app/api/cron/
- Protect it with
SUPABASE_SERVICE_ROLE_KEY
- Register the pg_cron job in Supabase (SQL)
Step-by-step checklist
1. Create the API route
Create app/api/cron/<name>/route.ts. The structure is always the same:
import { NextRequest, NextResponse } from 'next/server'
export const maxDuration = 800
export async function POST(request: NextRequest) {
const authHeader = request.headers.get('authorization')
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!serviceRoleKey || authHeader !== `Bearer ${serviceRoleKey}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const result = await runMyTask()
return NextResponse.json(result)
} catch (err) {
const msg = err instanceof Error ? err.message : 'Erro interno'
console.error('[Cron] my-task failed:', msg)
return NextResponse.json({ error: msg }, { status: 500 })
}
}
Always POST, never GET. Always maxDuration = 800.
2. Implement the task logic in lib/
Keep the route thin. Put the real logic in lib/:
export async function runMyTask(): Promise<{ ok: boolean; processed: number }> {
return { ok: true, processed: 0 }
}
Existing examples: lib/automation.ts, lib/rss-automation.ts, lib/source-crawlers/runner.ts.
3. Log execution to automation_logs (when applicable)
If the task generates posts or runs a significant pipeline, log to automation_logs:
import { db } from '@/drizzle/db'
import { automationLogs } from '@/drizzle/schema'
const startedAt = Date.now()
let postId: number | null = null
let errorMsg: string | null = null
try {
const result = await runMyTask()
postId = result.postId ?? null
} catch (err) {
errorMsg = err instanceof Error ? err.message : 'unknown'
} finally {
await db.insert(automationLogs).values({
trigger: 'schedule',
status: errorMsg ? 'error' : 'success',
duration_ms: Date.now() - startedAt,
post_id: postId,
error: errorMsg,
})
}
4. Register the pg_cron job in Supabase
Run this SQL in the Supabase SQL editor (not in code — this is a one-time DB operation):
SELECT cron.schedule(
'my-task-cron',
'0 */6 * * *',
$$
SELECT net.http_post(
url := 'https://your-app.vercel.app/api/cron/my-task',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || current_setting('app.service_role_key')
),
body := '{}'::jsonb
);
$$
);
To store the service role key as a Supabase setting (do once):
ALTER DATABASE postgres SET app.service_role_key = 'your-service-role-key-here';
Common cron expressions:
- Every hour:
'0 * * * *'
- Every 6 hours:
'0 */6 * * *'
- Every day at 3am:
'0 3 * * *'
- Every 30 minutes:
'*/30 * * * *'
5. Set the environment variable
Ensure SUPABASE_SERVICE_ROLE_KEY is set in Vercel (production + preview):
vercel env ls
vercel env add SUPABASE_SERVICE_ROLE_KEY
Or add via Vercel dashboard → Project → Settings → Environment Variables.
What NOT to do
{
"crons": [
{ "path": "/api/cron/my-task", "schedule": "0 */6 * * *" }
]
}
export async function GET(request: NextRequest) { ... }
export async function POST(request: NextRequest) {
const result = await runMyTask()
return NextResponse.json(result)
}
export async function POST(request: NextRequest) { ... }
Existing cron routes (reference)
| Route | Lib function | Trigger |
|---|
app/api/cron/automation/route.ts | lib/automation.ts → runAutomationCycle() | pg_cron |
app/api/cron/rss/route.ts | lib/rss-automation.ts | pg_cron |
app/api/cron/source-crawlers/route.ts | lib/source-crawlers/runner.ts | pg_cron |
Use these as reference when implementing a new cron route.
Verification before finishing