AI Content Planning: Build a 90-Day Content Calendar
Back to Blog
Planning· 9 min read

AI Content Planning: Build a 90-Day Content Calendar

Plan 90 days of content with AI: set pillars and cadence, generate ideas as validated JSON, schedule by rule, and export a CSV Google Calendar imports.

NA
By NEPA AI
NEPA AI · Building autonomous systems for creators and businesses
#content planning#AI automation#content calendar#strategy#productivity

A 90-day content calendar is long enough to plan around launches and seasons, and short enough that it doesn't go stale. AI is genuinely useful for building one. It's fast at generating a wide pool of ideas, and code can turn those ideas into dated slots without the copy-paste. What AI can't do is decide what your audience needs or what you can realistically produce. Those decisions come first, and they're most of the work.

This guide walks through the full process: the planning inputs, a Python script that generates ideas as validated structured data with a local model, rule-based scheduling across 13 weeks, a CSV export that Google Calendar can import, and a monthly review loop. The script is a working starting point, not a product, and the timing depends mostly on how long your review takes.

Illustration 1 for ai-content-planning-build-90-day-calendar-in-30-minutes

Decide the inputs before you prompt anything

A model given "make me a content calendar for AI" will produce a plausible-looking calendar for nobody in particular. Write these down first. Together they become the planning brief the script sends with every request.

  1. One goal for the quarter. "Grow newsletter signups" or "rank for local AI setup guides," not five goals at once. The goal decides which ideas survive review.
  2. The audience, in one paragraph. Who they are, what they already know, and what they're stuck on. Include the words they actually use. Your search queries and comments are the best source.
  3. Three to five content pillars. Recurring topic areas you can speak to with real experience. Fewer pillars give each one enough depth to build authority.
  4. A cadence you can sustain. Count the hours you actually have per week, then plan to about 80% of that. A calendar you abandon in week four is worse than a lighter one you finish.
  5. Fixed dates. Launches, events, holidays your audience cares about, and seasonal peaks in your niche. These go on the calendar before any generated idea does.
  6. What already exists. A list of your published titles, so the model doesn't propose things you've already covered.

How a 90-day calendar is structured

Ninety-one days is exactly 13 weeks, which keeps the math clean:

LayerCountWhat it does
Quarter goal1Filters every idea
Monthly themes3Give each month a focus, often building toward a launch
Pillars3–5Rotate within each month so no topic area goes quiet
Weekly slots13 weeks × your cadenceFixed days per format (for example, Tuesday blog, Monday and Thursday short video)
Fixed datesAs neededPlaced first; generated ideas fill around them
Illustration 2 for ai-content-planning-build-90-day-calendar-in-30-minutes

An example quarter for a creator who teaches local AI tools, with one blog post and two short videos a week, has 13 blog slots and 26 video slots. That needs an idea pool of roughly 60 to 80 candidates, because review typically cuts a good share of what any model proposes. That ratio is a planning assumption, not a measured rate. Adjust it after your first run.

Here's how that example quarter might look at the theme level. It's illustrative, built around a tutorial-series launch in week 7:

WeeksMonthly themeBlog focusShort-video focusFixed dates
1–4FoundationsSetup guides for each pillar's core toolQuick "it works" demos of each setupNone
5–8Launch runwayDeeper workflow posts that the new series builds onBehind-the-scenes clips of the series in productionSeries launch, week 7
9–13Results and costsCost comparisons and troubleshootingCommon errors and their fixesQuarter review, week 13

The themes give each month a reason to exist beyond "more posts." Month one earns trust with practical setup help. Month two builds toward the launch, so the launch post lands with readers who already have context. Month three answers the follow-up questions the first two months generate. Those are the questions your comments will be full of by then.

Illustration 3 for ai-content-planning-build-90-day-calendar-in-30-minutes

Step 1: Write the planning brief

Save this as brief.md and fill it in. The script reads it on every call.

Goal this quarter: Grow newsletter signups from tutorial readers.
Audience: Solo video creators on Windows PCs with mid-range NVIDIA cards. Comfortable
installing apps, not comfortable with Python. Frustrated by subscription costs.
Pillars: Local AI setup | Video workflows | Cloud GPU costs
Formats: blog (one per week), short video (two per week)
Voice: plain English, practical, no hype. Every idea must be something we can demonstrate.
Already published: "How to Run Whisper Locally", "Cloud GPU vs Local RTX 3090", ...
Avoid: news commentary, tool roundups without testing, anything we can't show on screen.

The "Avoid" line does more work than it looks like. Without it, models drift toward generic listicles and trend commentary. Google's guidance on helpful content asks whether you're writing about things simply because they seem to be trending, and whether you're mainly summarizing others without adding value. Those are the same ideas a careful planner cuts.

Step 2: Generate ideas as validated structured data

Free-text brainstorming is hard to schedule. Ask for JSON that matches a schema, then validate it. This example uses a local model through Ollama, whose structured outputs feature accepts a JSON schema in the format field. The same pattern works with any API that supports schema-constrained output.

# plan_quarter.py  (pip install ollama pydantic; ollama pull gemma3:12b)
import csv, datetime as dt, itertools, json
from typing import Literal
from pydantic import BaseModel
from ollama import chat

class Idea(BaseModel):
    title: str
    pillar: str
    format: Literal["blog", "short video"]
    angle: str            # one sentence: why this, why our audience
    search_query: str     # what someone would type to find it
    evergreen: bool

class IdeaPool(BaseModel):
    ideas: list[Idea]

BRIEF = open("brief.md", encoding="utf-8").read()

def generate_ideas(pillar: str, n: int = 25, model: str = "gemma3:12b") -> list[Idea]:
    schema = IdeaPool.model_json_schema()
    prompt = (f"{BRIEF}\n\nPillar: {pillar}\n"
              f"Propose {n} distinct ideas for this pillar. No two may answer the same question. "
              f"Return JSON matching this schema:\n{json.dumps(schema)}")
    resp = chat(model=model, messages=[{"role": "user", "content": prompt}],
                format=schema, options={"temperature": 0.7})
    return IdeaPool.model_validate_json(resp.message.content).ideas

The Literal type becomes an enum in the schema, so the model can only return formats your scheduler knows about. Pydantic raises an error if anything is missing or malformed, which is much better than discovering a broken row in week nine. Ollama's docs also recommend repeating the schema in the prompt, as shown, to ground the response.

Step 3: Cut the pool down

Generation is cheap, so be ruthless here. Remove obvious duplicates in code, then review the rest by hand:

Illustration 4 for ai-content-planning-build-90-day-calendar-in-30-minutes
def dedupe(ideas: list[Idea]) -> list[Idea]:
    seen, keep = set(), []
    for idea in ideas:
        key = " ".join(sorted(set(idea.search_query.lower().split())))
        if key not in seen:
            seen.add(key)
            keep.append(idea)
    return keep

Then keep an idea only if it passes all four checks:

  • Demand. Someone actually searches for or asks about this. Check the query in your Google Search Console Performance report, which groups clicks and impressions by the queries people typed. Your comments and DMs count too.
  • Ability. You can show it, test it, or speak from experience. If the post would need claims you can't back up, cut it.
  • Fit. It serves the quarter's goal. A great idea for a different goal can wait for next quarter.
  • Distinctness. It isn't the same answer as something you've already published. If it overlaps, it's an update to the old post, not a new slot.

Export the pool to a spreadsheet, mark keep or cut, and load the kept list back in.

Step 4: Schedule by rule, not by hand

Fixed dates go first. Then each format fills its weekdays from a queue that rotates pillars, so the same topic area never runs twice in a row:

def rotate_pillars(ideas: list[Idea]) -> list[Idea]:
    groups: dict[str, list[Idea]] = {}
    for idea in ideas:
        groups.setdefault(idea.pillar, []).append(idea)
    return [i for batch in itertools.zip_longest(*groups.values()) for i in batch if i]

def schedule(ideas, start: dt.date, slots: dict[str, list[int]], fixed: dict[dt.date, str]):
    """slots: {"blog": [1], "short video": [0, 3]}  (weekday numbers, Monday = 0)"""
    queues = {fmt: rotate_pillars([i for i in ideas if i.format == fmt]) for fmt in slots}
    rows = []
    for day in (start + dt.timedelta(days=d) for d in range(91)):
        if day in fixed:
            rows.append((day, fixed[day], "fixed", ""))
            continue
        for fmt, weekdays in slots.items():
            if day.weekday() in weekdays and queues[fmt]:
                idea = queues[fmt].pop(0)
                rows.append((day, idea.title, fmt, idea.pillar))
    return rows

If a queue runs dry before day 91, the calendar shows the gap, which tells you to generate more ideas for that format rather than quietly padding it.

Step 5: Export to Google Calendar, Notion, or a spreadsheet

Google Calendar imports CSV files if the headers are in English and match its format. Its import help page lists Subject and Start Date as the only required headers, with optional fields including All Day Event and Description:

def export_csv(rows, path="calendar.csv"):
    with open(path, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["Subject", "Start Date", "All Day Event", "Description"])
        for day, title, fmt, pillar in rows:
            w.writerow([f"[{fmt}] {title}", day.strftime("%m/%d/%Y"), "True", f"Pillar: {pillar}"])

if __name__ == "__main__":
    pillars = ["Local AI setup", "Video workflows", "Cloud GPU costs"]
    pool = dedupe([i for p in pillars for i in generate_ideas(p)])
    # ...review and trim `pool` here...
    rows = schedule(pool, dt.date(2027, 1, 4), {"blog": [1], "short video": [0, 3]},
                    {dt.date(2027, 2, 16): "Launch post: new tutorial series"})
    export_csv(rows)

Import into a dedicated "Content" calendar rather than your main one, so you can delete and reimport it cleanly when the plan changes. Notion and Airtable both import CSV too. A database view grouped by pillar makes gaps obvious.

Illustration 5 for ai-content-planning-build-90-day-calendar-in-30-minutes

Step 6: Review the calendar as a whole

Read the calendar top to bottom once before committing to it. Check for:

  • Clustering. Three hard tutorials in one week and nothing but light posts the next.
  • Dependencies. A post that assumes knowledge from one scheduled later. Swap them.
  • Launch runway. The two or three weeks before a fixed launch should build toward it.
  • Capacity spikes. Weeks where the planned work exceeds your real hours, often around holidays.
  • Repurposing hooks. Each blog post can usually feed one or more short videos. Pair them on the calendar so the production work is shared.

Once the plan holds up, production planning is a separate job. Our guide to content batch days covers turning a calendar into finished pieces in focused sessions, and AI content distribution covers adapting each piece for every platform you post on.

Keep it a rolling plan

A 90-day plan works best when you revisit it monthly:

  1. Pull last month's numbers. Look at search queries, top pages, and which videos held attention. How to Use AI to Analyze Your Content Performance covers summarizing these quickly.
  2. Move, don't delete. Push weak ideas to a backlog instead of discarding them. Context changes.
  3. Add one month at the end. Generate ideas for month four, so you always have about 90 days ahead.
  4. Update the brief. New published titles go in the "Already published" line, and new audience language goes in the audience paragraph.

When the plan breaks mid-quarter

Something will break it: a sick week, a tool update that invalidates a tutorial, or a post that takes off and deserves follow-ups. Decide in advance how you'll respond, so a disruption doesn't turn into a rebuild.

  • A missed week. Don't compress two weeks of work into one. Slide the remaining slots forward, and drop the lowest-priority idea from the end of the quarter instead of the next one in line.
  • An outdated topic. If a tool changes before its slot comes up, swap the idea for its replacement in the same pillar. The rotation stays intact, and the old idea goes to the backlog with a note on why.
  • A breakout post. Use one or two open slots for follow-ups, such as the questions in its comments or a deeper version of its best section. Don't rewrite the whole calendar around one result. Check the next month's numbers first.
  • A fixed date that moves. Rerun the scheduling step with the new date. Because scheduling is code, moving a launch takes a minute, not an afternoon of dragging calendar entries.

Write down each change and its reason in the review notes. By the end of the quarter, that log shows which assumptions in the brief were wrong, which is the most useful input for the next one.

Common mistakes

Planning more than you can produce. The most common failure. Plan to sustainable capacity and treat extra output as a bonus.

Letting the model choose the strategy. AI expands options. It doesn't know your goal, audience, or what you can demonstrate unless the brief tells it.

Copying "best posting times" from a blog post. Generic best-time charts aren't your audience. Use your own platform analytics once you have a few weeks of data.

Treating the calendar as fixed. News, launches, and results will change it. The point of the plan is that changes are deliberate instead of last-minute.

Skipping the review pass. Every generated idea needs a human check against demand, ability, fit, and distinctness. That's where the quality comes from.

FAQ

Why 90 days instead of a month or a year? A month is too short to plan around launches or build a topic cluster. A year goes stale before you reach the back half. Ninety days, revisited monthly, balances the two.

Which model should I use? Any model that supports schema-constrained output works. A 12B-class local model such as gemma3:12b (an 8.2 GB download) is a reasonable starting point and fits on a GPU with 12 GB or more of VRAM. A hosted model works too if you're comfortable sending the brief to an API. The review step matters more than the model.

Can AI pick the best posting times? Not reliably without your data. Start with a consistent schedule, then adjust using your own analytics. Our guide to AI social media scheduling covers scheduling tools.

How many ideas should I generate? Plan for two to three candidates per slot. The cut rate depends on your niche and brief, so measure it on your first quarter.

Where do the ideas come from if the model runs out? Your audience. Comments, support questions, and search queries are better seed material than another prompt. AI for Content Research covers mining those sources.