U Ranket
⌘K

Content calendar

How keywords get scheduled into the calendar, how plans get fanned out, and how the daily cron publishes them.

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:

  1. Counts unscheduled days in the next 30-day horizon, based on your cadence
  2. Queries keyword_opportunities for top-scoring available keywords (sorted by opportunity_score DESC)
  3. Picks up to N keywords (where N = unscheduled days)
  4. Creates one content_calendar row per keyword, with the target date
  5. Updates each keyword’s status from availablescheduled

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:

  1. Runs research stage (SERP fetch, PAA, related kws)
  2. Scrapes the top 5 competitor URLs
  3. Generates strategy via Claude Sonnet (format, word count, outline)
  4. Generates brief via Claude Sonnet (section spec, FAQ, internal links, comparison tables, TL;DR)
  5. Extracts data points via Claude Haiku (12-20 cite-able facts)
  6. Stores everything in the calendar row’s plan JSONB 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:

  1. Queries content_calendar for rows where:
    • scheduled_for <= today
    • status = 'scheduled'
    • plan IS NOT NULL
  2. For each, enqueues a content.generate job
  3. The job runs draft → polish → images → assemble
  4. On success, fires the CMS webhook
  5. Marks the row published (or ready if 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:

CadenceDays/weekRows per 30-day horizon
DailyMon-Sun (7)~30
MWFMon / Wed / Fri (3)~13
Tue+ThuTue / Thu (2)~9
WeeklyMon 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 available for 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 = null on 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

StatusMeaning
scheduledOn the calendar, plan optional, not yet generated
generatingPipeline is running for this row
readyArticle generated, awaiting webhook delivery (or manually approved)
publishedArticle delivered to CMS successfully
failedPipeline broke; needs manual retry or dismissal
dismissedManually removed; keyword goes back to available

Optimization runner

Beyond new articles, Ranket also runs a content optimization job. This:

  1. Reads your existing published articles from articles (status=‘published’)
  2. 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 failed until manual retry)

Was this page helpful?