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
| Function | Deliver approved social posts to connected social platforms on schedule or on demand |
| Type | Worker — Publishing |
| Status | To Build |
| Priority | P1 — Table-Stakes |
| Queue | agent__social-publisher |
| Concurrency | 10 |
| Timeout | 2 min |
| Est. cost / task | ~$0 (platform API call; no LLM inference) |
| Plan | Pro+ |
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
| Platform | Provider Package | API Used | Post Types Supported |
|---|---|---|---|
@leadmetrics/provider-meta | Graph API v21 — Pages Feed | Text, image, link | |
@leadmetrics/provider-meta | Graph API v21 — Media Create + Publish | Image, carousel, reel (video in v2) | |
@leadmetrics/provider-linkedin | LinkedIn Marketing API v2 — UGC Posts | Text, image, article link | |
| X (Twitter) | provider-x (new) | X API v2 — Tweets | Text, image (up to 4) |
| TikTok | provider-tiktok (new) | TikTok Content Posting API | Video only (v2) |
| Google Business Profile | @leadmetrics/provider-google | My Business API — Posts | Text, 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 aboveJob 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
| Scenario | Behaviour |
|---|---|
| Platform API returns 5xx | Retry 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 exhausted | publishError set; DM portal notification + dashboard warning badge on the post |
scheduledAt in the past at job creation | Allow up to 10 min grace period; if older than 10 min, mark as missed_schedule and do not publish |
Platform-Specific Constraints
| Platform | Max text length | Image requirements | Special handling |
|---|---|---|---|
| 63,206 chars | JPG/PNG, max 4 MB | Page access token (not user token) required | |
| 2,200 chars | JPG/PNG, min 320px, max 1440px, aspect 4:5 to 1.91:1 | Two-step API: create media → publish | |
| 3,000 chars | JPG/PNG, max 5 MB | author must be the Page URN | |
| X | 280 chars | JPG/PNG/GIF, max 5 MB | Thread support in v2 |
| GBP | 1,500 chars | JPG, min 250px square | Post 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:
Scheduled→Publishing→Published(with platform link) /Failed(with error tooltip) - “Publish Now” button visible on all
client_approvedposts 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
| Method | Path | Description |
|---|---|---|
POST | /tenant/v1/social/:id/publish | Publish immediately (enqueue with delay = 0) |
POST | /tenant/v1/social/:id/schedule | Update scheduledAt, re-enqueue BullMQ delayed job |
POST | /tenant/v1/social/:id/cancel-schedule | Remove from BullMQ queue; set scheduledAt = null |
GET | /tenant/v1/social/:id/publish-status | Return current publish status and platform link |
Open Questions
| ID | Question | Priority |
|---|---|---|
| OQ-SP-1 | X 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-2 | TikTok 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-3 | Instagram Reels (video) requires a different API flow. v1 supports image posts only; video support is v2. | Post-launch |
| OQ-SP-4 | Rate 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
- Add
platformPostId,platformPostUrl,publishedAt,publishError,publishAttemptstoSocialPost(migration) - Create
agent__social-publisherBullMQ queue inpackages/queue/src/types.ts - Create
apps/api/src/workers/social-publisher.worker.ts— platform dispatcher; routes to per-platform handler - Implement Facebook + LinkedIn publishers first (most common platforms in current tenant base)
- Add
POST /tenant/v1/social/:id/publishroute (immediate enqueue)
Phase 2 — Scheduled Publishing
- Hook
client_approvedstatus transition in blog/social post approval flow to auto-enqueue delayed BullMQ job - Add
POST /tenant/v1/social/:id/scheduleandcancel-scheduleroutes - Dashboard Social page: status badge updates via WebSocket
social:publishedevent
Phase 3 — Remaining Platforms
- Implement Instagram publisher (two-step media create → publish flow)
- Implement GBP publisher
- Implement X publisher (flag as optional/plan-gated until API tier is resolved)
Phase 4 — Monitoring
- Dashboard: failed publish warning badges + retry button
- DM portal notification for publish failures
- Add publish success/failure metrics to the Report Writer data sources