Scheduler
Schedules pull from your approved-content queue and publish at the cadence you set. Pause, archive, and runway warnings included.
Status: M4 (manual one-off scheduling) and M5 (Schedule entity + builder) shipped. M6–M11 in progress — see
docs/technical/sprint-plan.md.
Schedules (M5, top-level)
Click Schedule in the dashboard nav → land on /schedule. Each Schedule is a named, persistent, auto-publishing rule with its own:
- Cadence — days of the week + times of day (e.g. "Mon–Fri at 9am and 5pm")
- Time zone — IANA name; cadence times are interpreted in this TZ so a Pacific schedule keeps firing at 9am Pacific regardless of where you log in from
- Source —
Manual(you add specific approved posts) orCampaign pipeline(VibeDay auto-fills from a campaign's approved drafts) - Rotation policy —
Once-only(each generation publishes at most once) orReusable after N days - Targets — per-Schedule list of connected platform accounts. Different schedules can target different account combinations.
- Runway warnings — alerts when the predicted content queue runs low (default: 5 days)
- Lifecycle status —
Active,Paused,No content,Completed,Archived
Building a schedule
- Click + New schedule on
/schedule - Fill in the builder:
- Name — pick something descriptive ("Daily drumbeat", "Q4 launch push")
- Source — Manual or Campaign pipeline (pick the source campaign if the latter)
- Cadence — toggle days of week, add/remove times of day, pick start + optional end date, pick timezone
- Targets — checkbox each connected account this schedule should publish to
- Rotation — once-only (default) or reusable after a cooldown
- Runway warning — set the threshold (default 5 days) or 0 to disable
- Click Create schedule → land on the schedule's detail page
Schedule lifecycle
- Pause any active schedule from its detail page — the cadence resolver skips it and no new posts get scheduled (existing scheduled posts continue unless cancelled individually)
- Resume a paused schedule to return it to active
- Archive a schedule when you're done with it — keeps the history; hidden from the default list
Calendar view (M6)
The Schedule detail page (/schedule/[id]) now shows a week calendar of every scheduled post in this Schedule. Each day is a column with:
- Date header (today highlighted in sky blue, past days dimmed)
- Posts for that day, listed chronologically as cards
- Each card shows: image thumbnail, platform-icon row (FB/IG/TT), scheduled time, status dot, first ~100 chars of the Instagram caption
- Click a card → opens the post's full generation detail page
Adding posts to the schedule manually
Until the M9 cadence resolver lands, posts don't auto-fill from the cadence. Until then (and any time you want to insert a manual post):
- Top-right "+ Add post" button on the calendar, OR
- Click "+ Add" inside any empty future day cell
This opens a modal:
- Approved post — radio list of READY generations not yet scheduled. For CAMPAIGN_PIPELINE schedules, filtered to that campaign's generations. For MANUAL schedules, shows all approved across the workspace.
- When — datetime picker (your local time, must be in the future)
The schedule's configured target accounts are used automatically — you don't pick targets per post.
Calendar navigation
- ‹ / › prev/next week
- Today button jumps back to current week
- URL carries
?week=YYYY-MM-DDso calendar views are deep-linkable / sharable
Drag-and-drop + click-to-edit (M7)
You can now reorganize your week visually:
- Drag a card from one day column to another. Press and hold (or click and drag), drag over the target day, release. The card's date moves; the time of day stays the same. The target column highlights as you hover.
- Drag is keyboard-accessible — focus a card with Tab, press Space to pick it up, arrow keys to move between days, Space again to drop.
- Click the time on any card → popover with a datetime picker for precise time adjustments + a "Cancel scheduled post" link.
Conflict handling:
- If you try to drop a post at the exact same minute as another scheduled post on the same account, VibeDay blocks the move and shows an error. Move it a minute or two off.
- If you move a post within 30 minutes of another scheduled post on the same account, VibeDay allows the move but warns you with a toast. Useful for back-to-back posts; you can adjust if not intentional.
Past days dim out and reject drops. Cards with status other than SCHEDULED (PROCESSING, COMPLETED, FAILED) aren't draggable — only scheduled-but-not-yet-published posts can be moved.
Cadence resolver + runway prediction (M9)
Campaign-pipeline schedules auto-fill themselves. When you create one with a source campaign, VibeDay walks the next 7 days of your cadence and creates a ScheduledPost for each slot, pulling READY generations from that campaign in order. The resolver runs:
- Immediately when you create or resume a schedule (so the calendar fills in within seconds — no waiting)
- Once daily at 00:15 UTC for every ACTIVE schedule across the system (catches anything new approved overnight, advances the 7-day window each day)
- On demand via the Refresh queue button on the schedule detail page — useful after you approve a batch of drafts and want them slotted in now instead of waiting for the cron
Manual schedules don't auto-fill. You add posts yourself from the calendar's + Add button. The resolver still runs to compute your runway based on the upcoming posts you've added.
Runway prediction
The new Runway panel at the top of every schedule detail page shows:
- Runway in days — how long your current queue will keep publishing before you run out. Color-coded: green = healthy, amber = below your warning threshold, red = empty.
- Upcoming post count — total SCHEDULED posts queued for this schedule
- Slots / week — your cadence rhythm
- Last checked — when the resolver last ran
The runway is the first cadence slot you can't fill. Example: cadence "Mon/Wed/Fri at 9 AM" = 3 slots/week. If you have 6 approved generations queued, runway = 2 weeks.
Rotation policy
- Once-only: each approved generation publishes exactly once and is then retired from the pool. CANCELLED posts free the generation back up.
- Reusable after N days: a generation can be re-scheduled after a cooldown window. Use this for evergreen content you're happy to re-share.
Notifications
When your runway drops below your threshold (default 5 days), VibeDay sends a Schedule queue running low notification (in-app + email by default). When it hits 0, you get Schedule out of content and the status flips to OUT_OF_CONTENT. The moment you approve more content and the resolver runs, status flips back to ACTIVE. The low-warning is rate-limited to once per 20 hours per schedule so the daily cron doesn't spam you.
Publishing runtime (M10)
When a scheduled time arrives, VibeDay publishes for you:
- Each ScheduledPost fires its own deferred Inngest event at the exact scheduledFor time — no big polling cron, no minute-by-minute scan
- Per-target publish: a post targeting Facebook + Instagram makes two independent platform calls; each gets its own
Publicationrow with the result - Per-target outcomes show up on the generation detail page under Schedule: each target row gets a
POSTED/FAILEDbadge, and POSTED rows include a View → link to the live post on Facebook / Instagram / TikTok - Failures don't take down the whole post — partial-success is
COMPLETEDwith the failed targets listed; full-failure flips toFAILED - The publisher distinguishes between transient errors (rate limits — retried automatically up to 3 times), permanent errors (copyright rejection — recorded, no retry), and token errors (flips the account to needs reconnect and sends a notification)
Cancel + reschedule still work cleanly: cancel flips DB status so the deferred event no-ops when it fires; reschedule sends a fresh event for the new time, and the old event's stale-check skips it.
Note on App Review: Meta pages_manage_posts / instagram_content_publish and TikTok video.publish scopes are still in App Review. Until they're approved, the platform APIs reject our requests with permission errors — the runtime correctly surfaces those as FAILED publications with the message "App Review pending; posts will start going through automatically once approval lands." The moment the scopes land, everything works without code changes.
Polish (M11)
A few quality-of-life additions that round out the publishing flow:
- Retry failed targets. Every FAILED Publication on the generation detail page now has an inline Retry button. Click it and VibeDay re-fires just that target through the publish runtime — no need to reschedule the whole post or worry about double-publishing the targets that already succeeded. The post's status updates automatically (a partial-failure that becomes fully successful flips back to Completed).
- Multi-platform per-post preview. The generation detail page now shows side-by-side mockups of what the post will look like on Facebook, Instagram, and TikTok — using the platform-specific caption that the publisher will actually send (with hashtags appended). Catches issues like "the Facebook caption is fine but the Instagram one is empty" before you schedule.
- Schedule list filtering. The schedule list now has Active / All / Archived tabs with row counts. Default is Active so you only see what's running today; flip to All for paused + out-of-content schedules, Archived for the historical record.
- Stuck-publish recovery. If the publisher crashes mid-publish or a deploy interrupts it, a 15-minute background reaper finds posts stuck in PROCESSING > 10 minutes and flips them to FAILED with a clear "publish timed out — click Retry" message. So a deploy can never leave a post in limbo.
Not yet live
- Month view + brand/campaign/platform filters on the calendar — follow-up
What's shipped (M4)
Once you've approved a generation (status: READY) and connected at least one platform account (Settings → Platform connections), the generation detail page shows a Schedule card.
Scheduling a single post
- Click Schedule this post on a READY generation
- A modal opens with two fields:
- When — datetime picker (your local time, must be in the future)
- Publish to — checkbox list of every connected, ACTIVE platform account (Facebook Pages, Instagram accounts, TikTok accounts)
- Pick at least one account → Schedule
- The scheduled post appears under Upcoming in the Schedule card with the time + target list
Cancelling
Each upcoming scheduled post has a Cancel link. Click it → confirm → the row flips to CANCELLED and is moved to the History section. The post will not publish.
Re-scheduling
Cancel and create a new schedule. (Direct reschedule lands in a follow-up.)
Status badges on scheduled posts
- SCHEDULED (sky blue) — queued for the future
- PROCESSING (amber) — currently being published (M6+)
- COMPLETED (green) — published successfully (M6+)
- FAILED (red) — publish failed; the failure reason shows below the row (M6+)
- CANCELLED (grey) — cancelled by user before publish
What's NOT yet active
- The actual publishing doesn't fire yet — M6 wires the Inngest runtime that picks up SCHEDULED rows at their scheduled time and calls the platform API. Until then, rows sit in SCHEDULED status.
- Recurring schedules (cadence — "post 2x daily forever") land in M5.
- Calendar / queue views at
/scheduleland in M8.
Planned (M5 — recurring posting plans)
You'll be able to set up a Posting Plan that auto-publishes from your approved topic queue on a cadence:
- "Post 2x daily at 9am and 5pm, M-F, forever"
- "Post once a week on Sunday at 6pm"
- "Post daily for the next 30 days"
Each plan has its own target accounts (Plan A → Instagram only, Plan B → Facebook + TikTok), brand / campaign filters (only pull from a specific brand or campaign's approved generations), and rotation rules (default: each generation is published only once; you can mark specific ones for reuse).
Queue depth notifications (M7)
VibeDay calculates how many days of approved content your active plans have available. When the runway drops below the threshold (default: 5 days), you'll get:
- An in-app banner on the dashboard
- An email warning you to approve more drafts or add to the topic pipeline
Failure reporting (M7)
When a publish fails, you'll get:
- Email with the platform's actual error message
- A suggested fix where possible (e.g., "TikTok rejected the post for copyright. Disable this topic or edit the content.")
- The failed scheduled post stays visible with the failure reason so you can retry or skip
For the technical shape (DB models, eligibility resolver, action catalog), see docs/technical/data-model.md and docs/technical/server-actions.md.
The scheduler is where you take a READY generation and queue it to publish on a specific date and time to specific platform accounts.
Intended shape
- Schedule a generation: from the generation detail page, pick a date + time + target platform account(s) → creates a
ScheduledPost - Calendar view: week / month grid of upcoming publishes
- Queue view: linear list of upcoming, processing, completed, failed publishes
- Per-target publication tracking: each scheduled post can target multiple platform accounts; we track success / failure per target with retries
Dependencies (must land first)
- OAuth flows (Sprint 4) — Meta + TikTok OAuth callbacks at
/api/oauth/[platform]/callback/. Without connected platform accounts, there's nothing to publish to. - Platform Account model — encrypted token storage already in the schema (
PlatformAccount), but the connect flow isn't built yet. - Inngest publish job — background job that pulls due
ScheduledPostrows, decrypts tokens, calls the platform API, writesPublicationrows withexternalPostId.
Eligibility for scheduling
Only generations with status: READY will be scheduling-eligible. DRAFT generations must be approved first (already in place — see generations.md).
A future eligibility rule (Sprint 5+) will block re-publishing a topic that's already been posted from a previous generation.
This placeholder will be replaced with full documentation when the scheduler ships.
Email support@vibeday.com with what you're trying to do and a screenshot if it helps — we'll write back within one business day.
- BrandsSet up brand voice, tone, forbidden words, and visual identity. Every AI generation reads this so output sounds like you.
- CampaignsGroup related topics into a campaign with shared context — useful for product launches, themed series, or seasonal pushes.
- TopicsSeed the content pipeline with topic ideas. Add 10 by hand or 50 via AI Topic Designer; each becomes a generation when you hit Generate.
- GenerationsFrom draft to approval: review captions, regenerate the image, iterate on locked fields, then push READY so the scheduler can pick it up.
- NotificationsInbox for failed publishes, reauth prompts, content alerts. Configure per-type email delivery from settings.