Skip to Content
Content ToolkitSocial Publishing

Social Publishing

[To Build] · agent__social-publisher · BullMQ delayed job · platform provider APIs

Publishes approved and scheduled social media posts to connected social platforms. When a SocialPost reaches its scheduledAt time, a BullMQ delayed job fires and delivers the post to the appropriate platform via its API. Also supports on-demand “Publish Now” from the dashboard.

Related: Social Post Writer · Channels · Campaigns · Content Toolkit Overview


Overview

FunctionDeliver approved social posts to connected social platforms on schedule or on demand
TypeWorker — Publishing
StatusTo Build
PriorityP1 — Table-Stakes
Queueagent__social-publisher
Concurrency10
Timeout2 min
Est. cost / task~$0 (platform API call; no LLM inference)
PlanPro+

Why This Is Needed

Social posts are generated by social-post-writer, reviewed and approved through the HITL workflow, and given a scheduledAt timestamp by the Social Calendar Planner — but they are never actually sent anywhere. This is a documented gap in docs/campaigns/README.md and the most visible operational hole in the product: content that clients and DMs believe is “scheduled” silently sits in the database.


Supported Platforms

PlatformProvider PackageAPI UsedPost Types Supported
Facebook@leadmetrics/provider-metaGraph API v21 — Pages FeedText, image, link
Instagram@leadmetrics/provider-metaGraph API v21 — Media Create + PublishImage, carousel, reel (video in v2)
LinkedIn@leadmetrics/provider-linkedinLinkedIn Marketing API v2 — UGC PostsText, image, article link
X (Twitter)provider-x (new)X API v2 — TweetsText, image (up to 4)
TikTokprovider-tiktok (new)TikTok Content Posting APIVideo only (v2)
Google Business Profile@leadmetrics/provider-googleMy Business API — PostsText, event, offer

v1 scope: Facebook, Instagram, LinkedIn, GBP. X and TikTok are P2 within this feature.


How It Works

SocialPost reaches client_approved status with a scheduledAt timestamp set API creates a BullMQ delayed job: delay = scheduledAt - now() queue: agent__social-publisher jobId: social-post-{socialPostId} (deduplication key) At scheduledAt time, BullMQ fires the job social-publisher worker: 1. Load SocialPost + connected SocialPost.channel (ConnectedChannel) 2. Decrypt OAuth token from ConnectedChannel.tokenInfo 3. Refresh token if expiry < 10 min away 4. Call platform API with post content + media URLs 5. Store platformPostId + platformPostUrl + publishedAt on SocialPost 6. Emit WebSocket event: social:published { socialPostId, platform, platformPostUrl } Dashboard Social page updates post status to "Published" with platform link

“Publish Now” flow (bypass schedule):

DM or admin clicks "Publish Now" on an approved SocialPost API: POST /tenant/v1/social/:id/publish Enqueues job with delay = 0 (immediate) Same worker flow as above

Job Payload

interface SocialPublishJob { tenantId: string; socialPostId: string; connectedChannelId: string; platform: 'facebook' | 'instagram' | 'linkedin' | 'x' | 'tiktok' | 'gbp'; scheduledAt: string; // ISO timestamp — informational, job fires at this time via BullMQ delay }

Publish Result

interface SocialPublishResult { socialPostId: string; platform: string; publishedAt: string; // ISO timestamp platformPostId: string; // native ID from the platform platformPostUrl: string | null; // public URL — null if platform does not return it success: boolean; error?: string; // populated on failure }

DB Changes

Add to the SocialPost model in packages/db/prisma/schema.prisma:

platformPostId String? platformPostUrl String? publishedAt DateTime? publishError String? // last error message if publish failed publishAttempts Int @default(0)

Error Handling & Retry Strategy

ScenarioBehaviour
Platform API returns 5xxRetry with exponential backoff — 3 attempts: 1 min, 5 min, 15 min
Token expired (refresh fails)Mark publishError = "Token refresh failed — reconnect channel", no retry; DM alert sent
Platform API returns 4xx (bad content)No retry; store error, mark publishError; DM notified with platform error message
All 3 retries exhaustedpublishError set; DM portal notification + dashboard warning badge on the post
scheduledAt in the past at job creationAllow up to 10 min grace period; if older than 10 min, mark as missed_schedule and do not publish

Platform-Specific Constraints

PlatformMax text lengthImage requirementsSpecial handling
Facebook63,206 charsJPG/PNG, max 4 MBPage access token (not user token) required
Instagram2,200 charsJPG/PNG, min 320px, max 1440px, aspect 4:5 to 1.91:1Two-step API: create media → publish
LinkedIn3,000 charsJPG/PNG, max 5 MBauthor must be the Page URN
X280 charsJPG/PNG/GIF, max 5 MBThread support in v2
GBP1,500 charsJPG, min 250px squarePost type: STANDARD, EVENT, OFFER

The social-post-writer agent already respects these character limits. The publisher does not truncate — if a post exceeds the limit it will fail with a 4xx; this indicates a prompt regression to investigate.


Scheduling UI Changes

Dashboard Social page:

  • Post status badge: ScheduledPublishingPublished (with platform link) / Failed (with error tooltip)
  • “Publish Now” button visible on all client_approved posts with a schedule
  • “Reschedule” button to update scheduledAt (cancels the existing BullMQ job, re-enqueues with new delay)

Dashboard Calendar page:

  • Published posts show a tick indicator; failed posts show a warning icon
  • Clicking a failed post opens a detail panel with the error and a “Retry” button

New API Routes

MethodPathDescription
POST/tenant/v1/social/:id/publishPublish immediately (enqueue with delay = 0)
POST/tenant/v1/social/:id/scheduleUpdate scheduledAt, re-enqueue BullMQ delayed job
POST/tenant/v1/social/:id/cancel-scheduleRemove from BullMQ queue; set scheduledAt = null
GET/tenant/v1/social/:id/publish-statusReturn current publish status and platform link

Open Questions

IDQuestionPriority
OQ-SP-1X API access: the free tier only allows posting to the authenticated user’s own account. Most agency use cases require posting to a client’s account — does this require X API Basic ($100/mo) per tenant?Pre-launch
OQ-SP-2TikTok Content Posting API requires app review and a business account. v1 scope should exclude TikTok unless tenants already have approved apps.Pre-launch
OQ-SP-3Instagram Reels (video) requires a different API flow. v1 supports image posts only; video support is v2.Post-launch
OQ-SP-4Rate limits are per-page / per-account, not per app. High-volume tenants with many clients may hit Facebook’s 200 calls/hour limit. Needs per-channel rate limit tracking.Post-launch

Implementation Phases

Phase 1 — Foundation

  1. Add platformPostId, platformPostUrl, publishedAt, publishError, publishAttempts to SocialPost (migration)
  2. Create agent__social-publisher BullMQ queue in packages/queue/src/types.ts
  3. Create apps/api/src/workers/social-publisher.worker.ts — platform dispatcher; routes to per-platform handler
  4. Implement Facebook + LinkedIn publishers first (most common platforms in current tenant base)
  5. Add POST /tenant/v1/social/:id/publish route (immediate enqueue)

Phase 2 — Scheduled Publishing

  1. Hook client_approved status transition in blog/social post approval flow to auto-enqueue delayed BullMQ job
  2. Add POST /tenant/v1/social/:id/schedule and cancel-schedule routes
  3. Dashboard Social page: status badge updates via WebSocket social:published event

Phase 3 — Remaining Platforms

  1. Implement Instagram publisher (two-step media create → publish flow)
  2. Implement GBP publisher
  3. Implement X publisher (flag as optional/plan-gated until API tier is resolved)

Phase 4 — Monitoring

  1. Dashboard: failed publish warning badges + retry button
  2. DM portal notification for publish failures
  3. Add publish success/failure metrics to the Report Writer data sources

© 2026 Leadmetrics — Internal use only