Skip to main content

Post Scheduling — Technical Reference

← Back to Post Scheduling

Post scheduling (Fase D.6 / roadmap 4.14) adds a nullable scheduledAt to posts plus a cron worker that releases due posts. Before this, posts only had isPublished: Boolean (default true) and there was no future-publish concept at all.

Where this lives

Backend

Frontend

Technical implementation checklist

  • scheduledAt on Post / PostCreateInput; createPost holds future posts unpublished
  • myScheduledPosts query — ScheduledPostsPage.tsx
  • cancelScheduledPost mutation — cancel button on the same page
  • updateScheduledPost mutation (ScheduledPostUpdateInput: text/visibility/scheduledAt) — edit button on the same page
  • scheduled-post-publisher.service.js cron worker — publishes due posts every minute
  • getByUser hides scheduled posts from non-owner viewers; the owner sees their own scheduled posts on their profile grid, badged with the release time
  • Scheduling stories — StoryCreateInput.scheduledAt (CreateStoryModal.tsx); the story's 24h expiresAt window starts when the sweep publishes it, not at creation
  • Scheduling clips — a clip is just a post whose only media is a video (type: 'clip', set automatically in createPost), so it's already covered by regular post scheduling; no separate clip-scheduling UI

GraphQL API

# Schedule a post: pass a future ISO timestamp. Omit (or pass a past time)
# to publish immediately.
mutation CreateScheduledPost($input: PostCreateInput!) {
createPost(input: $input) { id scheduledAt isPublished }
}
# input: { text: "...", visibility: public, scheduledAt: "2026-08-01T15:00:00Z" }

# The caller's own scheduled (not-yet-published) posts, soonest first.
query MyScheduledPosts($limit: Int, $offset: Int) {
myScheduledPosts(limit: $limit, offset: $offset) {
id text scheduledAt visibility
media { mediaUrl thumbnailUrl mediaType }
}
}

# Cancel before it publishes (owner only, only while still scheduled).
mutation CancelScheduledPost($postId: ID!) {
cancelScheduledPost(postId: $postId)
}

# Edit a still-scheduled post's caption, visibility and/or time (owner
# only, only while still scheduled; scheduledAt must be in the future).
mutation UpdateScheduledPost($postId: ID!, $input: ScheduledPostUpdateInput!) {
updateScheduledPost(postId: $postId, input: $input) { id text scheduledAt }
}

# Schedule a story the same way; its 24h window starts when it's published.
mutation CreateScheduledStory($input: StoryCreateInput!) {
createStory(input: $input) { id visibility }
}

How releasing works

createPost/createStory store a future-scheduled post (or story) with isPublished: false and the chosen scheduledAt. Feed queries and other users' profile grids all filter isPublished: true, so nobody but the owner sees it before its time. Every minute, scheduled-post-publisher.service.js calls post.manager.js#publishDueScheduledPosts, which finds posts where isPublished = false AND scheduledAt <= now (getDueScheduled, backed by the scheduled_at index) and updates each to isPublished: true, scheduledAt: null — and, for a scheduled story (type === 'story'), also sets a fresh expiresAt 24h out from the actual publish moment, since a scheduled story doesn't start its 24h window until it's actually released. From that point the post/story is indistinguishable from one published normally. Worst-case latency is ~60 seconds past the scheduled minute. Each post is updated independently, so one failure doesn't block the rest of the batch.