Skip to content

AI campaigns

An AI campaign turns a set of prompts into a run of posts. You pick the prompts, the channels and an end date; it writes one post per prompt and spreads them across the calendar, all held for approval.

Once, or on a repeat

Build the campaign

Give it a name, pick your prompts and channels, and set an end date.

Choose the days and times

Pick the times of day and the weekdays it should run on.

Turn on Repeat, or leave it off

Off — the campaign generates when you press Run now, and that's it.

On — it runs again by itself on the days and times you picked, until the end date.

Missed runs are skipped, not caught up

If the app was down when a run was due, that run does not happen later. If two runs came due while it was down, only the most recent one goes ahead. Generating two batches to "catch up" is almost never what anyone wants, and each batch costs AI credits.

What you'll see on the card

  • Active — next 14/02 09:00 — it repeats, and that is when.
  • Active — it repeats, but no upcoming run has been worked out yet.
  • One-off — it only runs when you press Run now.
  • Generating… — a run is happening right now.
  • Finished — it passed its end date and stopped on its own.
  • Stopped — you turned it off.

Credits

Every run costs AI credits: one text generation per prompt, plus an image generation per prompt when the campaign makes images. A run that would take you over your monthly allowance is skipped rather than run half-way, and nothing is charged for it.

For developers

The schedule lives on ai_posts.recurrence as { enabled, times: ["HH:mm"], days: [0-6], timezone, last_occurrence } — typed rather than an RRULE, because the editor is a times × weekdays grid and cannot produce or render anything else. Campaigns created before this field existed are read out of the legacy data.scheduledTimes / data.selectedDays blob by resolveCampaignRecurrence, which is why no migration was needed; those come back with enabled: false, because a campaign configured months ago should not start spending on a decision nobody re-made.

generateCampaignRuns runs every ten minutes (campaign-recurrence, jobId: campaign-recurrence-10m). It is a due-time sweep, not a horizon generator: runCampaign calls a model, so producing thirty days of content up front would be both wasteful and stale. The catch-up window is two hours and the newest due slot wins.

Idempotency is a conditional update — findOneAndUpdate({ _id, "recurrence.last_occurrence": { $ne: occurrence } }) — so two replicas cannot both claim the same slot. Gates run in order before anything is spent: enabled → active → end date → workspace not locked → plan has ai.text → AI credits available.