VibeDayAI-Powered Social Media Management

Scheduler

Schedules pull from your approved-content queue and publish at the cadence you set. Pause, archive, and runway warnings included.

Status: M4–M11 SHIPPED (manual scheduling, Schedule entity + builder, week calendar + drag-drop, cadence resolver, Inngest publishing runtime, retry, stuck-processing reaper). Plus the 2026-06-15 additions below. Last updated: 2026-06-15

Current status (as-built — 2026-06-15)

The publishing runtime is fully live across Facebook, Instagram, TikTok, and YouTube (YouTube is video-only). The following supersede/augment the older sections below; where they conflict, this section is authoritative:

  • Declared post type (per account). The publish modal shows, and freezes, the declared post type — Feed, Reel, Story, Short — as a "Reel · 9:16"-style chip on each selected account row, so the scheduled post publishes to the exact native format you previewed (IG Story, FB Reel/Story, YouTube Short each go to their own endpoint). Frozen onto ScheduledPostTarget.postType; the publisher dispatches on it. See docs/functional/platform-preview.md.
  • Posting guardrails (pre-flight + publish-time). Before a post goes out, the asset is validated against the target platform's media spec (duration, file size, format, carousel count). The publish modal shows per-platform warnings (GuardrailWarnings) with a "post anyway" acknowledgement gate (warn, don't block). At publish time, the publisher short-circuits a doomed post — on a will_fail it skips the partner call and returns errorClass: "spec_violation" with the clear reason (src/lib/publishing/publisher.ts). See posting-guardrails.md.
  • Live failure reporting. The generation Schedule section and the week calendar now poll while any post is in flight (LiveRefresh / usePollingRefresh), so a post flips to POSTED / FAILED live — with the actionable reason + per-target Retry — without a manual reload.
  • Verified publish (no false success, 2026-08-09). Async platforms (TikTok Direct Post, Facebook video/reel) ACCEPT a post before it's actually live, so "accepted" is never shown as "Posted". Such a target sits in PROCESSING ("Publishing…") while the publish flow polls the platform's real status endpoint (confirmPublication), flipping it to POSTED only once the platform confirms it's live — or FAILED with the platform's own reason (e.g. TikTok photo_pull_failed, a spam/rate limit) if it didn't. A ~4-min poll budget plus the reaper guarantee it never sticks. The success notification + post-count usage fire only after confirmation. Instagram, Facebook photo/feed, and YouTube confirm synchronously and skip PROCESSING.
  • Failure classification (classifyFirstFailure) buckets failures as spec_violation / token / permanent_rejection / rate_limit / permissions / network / other for the POST_FAILED telemetry + notification.
  • Stuck-progress reaper is the general in-progress backstop (function id stuck-processing-reaper, 15-min cron): ScheduledPost PROCESSING→FAILED, Publication PROCESSING→FAILED (async-confirm interrupted) and Generation GENERATING→FAILED after 10 min, Asset PROCESSING→FAILED after 6h.
  • Publishing is per-post deferred Inngest events (each ScheduledPost fires its own event at scheduledFor with a stale-check), NOT a polling cron.

⚠️ The older "What's NOT yet active" (M4) and "Planned (M5/M7)" / "Intended shape" placeholder blocks near the end of this doc are superseded — those features (publishing runtime, recurring schedules, calendar, OAuth connect, failure reporting) are all shipped. Treat them as historical.

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
  • SourceManual (you add specific approved posts) or Campaign pipeline (VibeDay auto-fills from a campaign's approved drafts)
  • Rotation policyOnce-only (each generation publishes at most once) or Reusable 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 statusActive, Paused, No content, Completed, Archived

Building a schedule

  1. Click + New schedule on /schedule
  2. 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
  3. 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-DD so 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 Publication row with the result
  • Per-target outcomes show up on the generation detail page under Schedule: each target row gets a POSTED / FAILED badge, 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 COMPLETED with the failed targets listed; full-failure flips to FAILED
  • 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 (updated 2026-08-23): these scopes are approved and live — Meta pages_manage_posts / instagram_business_content_publish (verified in the App Review dashboard) and TikTok video.publish (app approved 2026-05-28, Content Posting audit approved 2026-07-31, public posting verified 2026-08-10). The only Meta items still Not approved are pages_manage_engagement (FB comment replies) and Human Agent (>24h DM replies) — see docs/technical/meta-app-review-resubmit-2026-08.md; neither blocks publishing. The paragraph below describes how the runtime behaves when a scope IS missing, which remains accurate for any future un-approved scope. 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

  1. On a READY generation, open the Publish split-button → Schedule for later (or use the "schedule for later" link under the platform preview).
  2. A modal opens:
    • When to publish — a datetime picker (your local time, must be in the future). It starts empty so the time is always a deliberate choice — a pre-filled default previously let a post get scheduled for a time the user never picked. A "Use tomorrow 9:00 AM" shortcut fills a sensible default in one click.
    • Publish to — checkbox list of every connected, ACTIVE platform account (Facebook Pages, Instagram accounts, TikTok accounts)
  3. Pick a time and at least one account. The Schedule button stays disabled until a valid future time is set, and once set it echoes the exact time (e.g. "Schedule for Fri, Aug 7 · 9:00 AM") so the outcome is unmistakable before you commit.
  4. The scheduled post appears under Upcoming in the generation's Schedule card, and on the Calendar (/calendar) alongside every other scheduled post. A "View on calendar →" link on the Schedule card jumps straight to its week.

Calendar vs Schedules (two surfaces)

The Publish nav splits scheduling into two distinct surfaces:

  • Calendar (/calendar) — a workspace-wide view of every scheduled post, whether you scheduled it individually (a one-off) or it came from a recurring schedule (auto). Toggle Week (full cards with thumbnails) or Month (a 6-week grid of compact chips — no thumbnails, so a busy month loads instantly). Preset tabs sit above a filter bar — narrow by source, platform, status, brand, campaign, or who scheduled it (all URL-encoded, so a filtered view is shareable). Built-in tabs All · Mine, plus saved views: set up any filter combo and hit “+ Save view” to keep it as a named tab you can re-apply in one click (saved to your account, synced across your devices; delete with the ×). Tag any post into a campaign from its Schedule card so it can be filtered on the calendar (optional — a post can stand on its own). In Week view you can drag a scheduled post to another day to reschedule it (keeps the time; only SCHEDULED posts are draggable, and you can't drop into the past), or click a post's time to reschedule/cancel with a picker. Posts scheduled by someone who has since left the team stay on the calendar, attributed as a former member. Approvers also see an amber "N awaiting approval" chip that jumps to the pending-approval list (unapproved posts aren't on the calendar — a gate blocks them until approved). This is where a one-off post lives (it isn't tied to any recurring Schedule).
  • Schedules (/schedule) — the recurring cadence rules that auto-pull approved drafts and publish them on a repeating schedule. A separate concept from the Calendar (which shows the resulting posts).

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 /schedule land 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 ScheduledPost rows, decrypts tokens, calls the platform API, writes Publication rows with externalPostId.

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.

YouTube daily capacity (pre-audit)

Until Google's API audit raises VibeDay's quota, YouTube accepts about 6 VibeDay publishes per day platform-wide. A post that hits the ceiling is not lost and nothing is wrong with it — its YouTube target shows "YouTube's daily publishing capacity is used up for today… will retry automatically tomorrow", and VibeDay re-publishes it automatically shortly after midnight Pacific when YouTube resets. The same applies when a channel hits YouTube's own per-channel daily upload cap. Other platforms in the same post are unaffected and publish normally.


Still stuck?

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.