The content calendar is the bridge between your discovered keyword pool and what actually publishes. Each row is one future article — a keyword, a target date, a plan, and a status.
The lifecycle
keyword_opportunities (status='available')
│
│ Calendar fill — picks top-scoring keywords for next N days
▼
content_calendar (status='scheduled', plan=null)
│
│ Plan fanout — content.plan job per row
▼
content_calendar (status='scheduled', plan={strategy, brief, facts, ...})
│
│ Daily cron — picks rows scheduled for today
▼
content_calendar (status='generating')
│
│ Generation pipeline (draft + polish + images + assemble)
▼
content_calendar (status='ready', article_id=...)
│
│ Webhook delivery → your CMS
▼
content_calendar (status='published')
Each transition is logged. Failures at any step mark the row failed with an error message; you can retry from the dashboard.
Calendar fill
When research completes (whether automatic weekly or manual), Ranket calls fillCalendar:
- Counts unscheduled days in the next 30-day horizon, based on your cadence
- Queries
keyword_opportunitiesfor top-scoringavailablekeywords (sorted byopportunity_scoreDESC) - Picks up to N keywords (where N = unscheduled days)
- Creates one
content_calendarrow per keyword, with the target date - Updates each keyword’s status from
available→scheduled
Cross-brand dedup
If the same keyword somehow got into both your available pool and a calendar row already (rare — happens if you re-import keywords), fill skips the duplicate. The unique constraint is on (brand_id, primary_keyword) for scheduled+planned entries.
Skip behaviour
If the keyword pool is shallow:
- Calendar fills as many days as it can
- Days without keywords are left empty (not scheduled at all)
- The next research refresh will surface new keywords; the next fill will populate those empty days
- The dashboard flags consecutive empty days as a “low pool” warning
Plan fanout
After fill, Ranket enqueues a content.plan job for every newly-scheduled row. Each plan:
- Runs research stage (SERP fetch, PAA, related kws)
- Scrapes the top 5 competitor URLs
- Generates strategy via Claude Sonnet (format, word count, outline)
- Generates brief via Claude Sonnet (section spec, FAQ, internal links, comparison tables, TL;DR)
- Extracts data points via Claude Haiku (12-20 cite-able facts)
- Stores everything in the calendar row’s
planJSONB column
Plans run immediately on fill — typically minutes after a research refresh. The daily cron later only has to execute the plan, not build it from scratch.
Why pre-plan?
A naive design runs all stages on the scheduled day. Problems:
- Slow (~10-15 min per article)
- Concentrated cost (every article incurs $0.13 of plan work on the day it generates)
- Coordinated failure (a DataForSEO outage at 09:00 UTC can stall the entire daily cron)
Pre-planning:
- Spreads work across the week
- Daily generation runs in ~3-5 min (just draft + polish + images + assemble)
- A plan failure on Monday doesn’t block Tuesday’s article (each row is independent)
- You can preview the planned brief days before the article generates
Daily cron
Every day at 09:00 UTC, the daily generation cron:
- Queries
content_calendarfor rows where:scheduled_for <= todaystatus = 'scheduled'plan IS NOT NULL
- For each, enqueues a
content.generatejob - The job runs draft → polish → images → assemble
- On success, fires the CMS webhook
- Marks the row
published(orreadyif webhook delivery failed)
The cron also has a recovery sweep that picks up rows stuck in generating for more than 30 minutes — usually a sign that a previous attempt crashed mid-pipeline. These get retried automatically up to 2 times before being marked failed.
Cadence math
Your chosen cadence determines how many calendar rows get created per fill:
| Cadence | Days/week | Rows per 30-day horizon |
|---|---|---|
| Daily | Mon-Sun (7) | ~30 |
| MWF | Mon / Wed / Fri (3) | ~13 |
| Tue+Thu | Tue / Thu (2) | ~9 |
| Weekly | Mon only (1) | ~4 |
The horizon is fixed at 30 days. If you want longer (60 / 90 days planned), change horizon in the brand settings.
Manual scheduling
You can override the auto-fill at any time:
- Add specific keyword to specific date — drag a keyword from the pool onto a calendar slot
- Reschedule — drag an existing entry to a new date
- Dismiss — remove a scheduled entry; the keyword goes back to
availablefor a future fill - Force regenerate — re-run plan or full generation for a specific entry
Manual changes don’t break auto-fill. The next fill respects your manual edits and fills around them.
Plan re-runs
If you change brand profile fields (niche, tone, audience, value propositions) after plans are stored:
- Existing plans stay as-is (they were built against the old profile)
- You can bulk re-plan from brand settings to wipe
plan = nullon all scheduled rows; the fanout cron re-plans them within the hour - The next research refresh also re-plans any new entries it creates
We don’t auto-replan on profile changes because the cost (~$0.13 × 30 entries = ~$4 per profile edit) adds up.
Failed entries
Common reasons a row can land in failed:
- Plan failure — usually a Claude rate limit or DataForSEO 5xx during research
- Generation failure — Claude returned malformed JSON (rare with current schemas), or image generation failed
- Webhook failure — your CMS endpoint returned non-2xx for 3 retries
Failed rows show the stage that broke (current_stage) and the error message. Click “Retry” to re-run from that stage. The keyword stays scheduled (not dropped from the calendar).
Status states
| Status | Meaning |
|---|---|
scheduled | On the calendar, plan optional, not yet generated |
generating | Pipeline is running for this row |
ready | Article generated, awaiting webhook delivery (or manually approved) |
published | Article delivered to CMS successfully |
failed | Pipeline broke; needs manual retry or dismissal |
dismissed | Manually removed; keyword goes back to available |
Optimization runner
Beyond new articles, Ranket also runs a content optimization job. This:
- Reads your existing published articles from
articles(status=‘published’) - For articles older than 90 days that have lost rankings (GSC integration required):
- Re-extracts current SERP top 10
- Generates an “update plan” — what to add, what to refresh, what’s outdated
- Regenerates the article body with the update
- Re-delivers via webhook (your CMS replaces the post)
This is opt-in per brand. The default is to focus on new articles; optimization is a Phase 2 add-on once you’ve published enough articles to make the work worthwhile.
Cost
Per calendar fill (30 keywords)
Calendar fill (DB only) $0.00
Per planned row
Research + scrape + strategy + brief + data points $0.13
30 rows $3.90
Per generation
Draft + polish + images + assemble $0.68
30 generations/month $20.40
Monthly total at daily cadence ~$24/month
Limits
- Maximum horizon: 60 days (default 30)
- Maximum 1 article per scheduled date (no double-scheduling on the same day)
- Plan retention: indefinite (plans stay on the row even after the article publishes)
- Failed retry attempts: 2 (then marked
faileduntil manual retry)