Skip to content

v4.9 — Facebook Social Publishing (Remy)

Facebook stops being a silent no-op for Remy — she now drafts real, human-approved link-preview posts on it, same as X and LinkedIn.

Summary

Remy, the social_publisher AI Workforce employee, already drafted scheduled posts for X and LinkedIn — but Facebook was explicitly gated behind "OG-image generation" work that never happened, and a second, unrelated gap meant Buffer delivery credentials weren't even wired into any deployed environment, so X and LinkedIn posts were silently failing to deliver too. This milestone closed both gaps at once.

Facebook now ships as a link-preview post (Route A): rather than generating and hosting a new image for every post, Remy attaches a link to an existing marketing page, and Facebook scrapes that page's own og:image/og:title/og:description to build the preview card. This needed zero new image-generation infrastructure — the tradeoff, explicitly accepted by the owner, is that every Facebook post's preview card looks the same (only the post text varies), since Remy has no natural per-post landing page to link to. Click-through is measured via a new UTM convention (utm_source=facebook) reviewed on the same weekly PostHog cadence Remy's other metrics already use.

Along the way, this milestone also merged a previously-unmerged Buffer GraphQL client rewrite (fixing the pre-existing X/LinkedIn silent no-op), live-verified that Buffer's saveToDraft behavior really does hold posts back as drafts rather than auto-publishing them, added Facebook support to the admin Review Queue (closing a real link-injection security gap along the way), and — as independent parallel work — adopted a closed, five-beat taxonomy for what Remy is allowed to post about. Two pieces were left unfinished, both found during this milestone's close audit: the new 1200×630 landscape preview-card image was designed and shipped, but never wired into the marketing page's og:image tags, so Facebook posts still show the old portrait screenshot today; and the dedicated Facebook admin review UI shipped, then was unintentionally deleted a few hours later by an unrelated route-cutover PR. See Tech Debt below for both.

Goal

Unblock Facebook as a delivery channel for Remy (social_publisher)'s scheduled social_post artifacts, shipping as link-preview posts (Route A) via the Buffer GraphQL API — with zero new image-generation runtime. Click-through is a stated goal; a content-invariant preview card is an accepted tradeoff (~156 identical-card posts/year).

Scope — What Shipped

  • Buffer GraphQL client merge + live verify (Phase 0) — the GraphQL client rewrite for Ai::SocialPublisherClient merged via PR #1605, replacing the stale REST client that 401'd against modern Buffer tokens; Phase 0 itself wired SOCIAL_AGGREGATOR_API_KEY/BUFFER_PROFILE_ID_* into the production deploy manifest (previously wired nowhere, silently no-op'ing X and LinkedIn delivery too), added human-gated rake tooling (buffer:print_verification_payload, buffer:create_verification_draft, buffer:check_draft_status), and used it to confirm live that saveToDraft really does hold a post as a draft instead of auto-publishing.
  • Facebook delivery path (Phase 1a) — Ai::SocialPublisherClient now sends Facebook's createPost payload (metadata.facebook.linkAttachment, always assets: [], with a forward-defense mutual-exclusivity guard for a future generated-image path). DeliverArtifactJob#deliver_social_post threads the artifact's link_url through to Buffer and permanently fails the post (no retry) if facebook is requested with a blank link_url, catching the problem at draft time instead of as a live Buffer error after an admin has already approved it.
  • Admin Review Queue support (Phase 1c) — the Review Queue's social_post panel recognized Facebook (label, character counter) and rendered a clickable link-preview card so an admin could inspect the real OG scrape before approving, with a scheme allowlist (http/https only) keeping the LLM-authored link_url from ever rendering as a javascript:/data: URI. This shipped surface no longer exists — see Tech Debt.
  • Remy's prompt + UTM measurement (Phase 1b, final phase) — Remy's employee description and task prompt now include Facebook and require a UTM-tagged link_url (utm_source=facebook&utm_medium=social&utm_campaign=remy-social) pointing at an existing marketing CTA whenever facebook is in channels — never an invented URL, never an in-app SPA route.
  • 1200×630 landscape og:image design — a new social card (marketing_landing/images/social/og-card.png) replacing the old 390×844 portrait phone screenshot as the intended Facebook/LinkedIn/X preview image.
  • Five-beat Content Beat Framework adoption (independent, parallel) — Remy's prompt now names a closed list of five beats (Feature reveal, Progress proof, Community story, Launch countdown, Behind the build) instead of three open-ended examples.

Phases

PhaseNameStatusPlansHighlights
0Buffer GraphQL client merge + live verifyShipped2 (PRs #1616, #1622, on top of the #1605 client rewrite)Fixed the pre-existing X/LinkedIn silent no-op; live-verified drafts don't auto-publish
1aFacebook delivery pathShipped1 (PR #1630)Client + job guard + deploy manifest wiring
1cAdmin Review Queue display⚠️ Shipped, then deleted by unrelated PR1 (PR #1629)Facebook label + safe link-preview card; closed a link-scheme injection gap — deleted ~3h later by PR #1638 (OBJ-1562), tracked at OBJ-1576
1bRemy prompt + UTM measurementShipped1 (PR #1639)Facebook channel + required UTM link_url; docs landed in the same PR
1200×630 og:image designShipped1 (PR #1615)New landscape social card asset, not yet wired into index.html
Beat taxonomy fixShipped1 (PR #1614)Closed five-beat list, independent of Facebook

Key Decisions

  • Link-preview (Route A) over a generated image, and over text-only — the owner confirmed click-through matters for Facebook, which rules out a text-only post; but building a real image-generation pipeline was out of scope for this milestone, so Remy reuses existing marketing pages' og: tags instead of hosting a new image per post. See docs/product/og-image-facebook-scope.md §1 for the full Buffer-schema-driven analysis of why this was the right tradeoff.
  • A content-invariant preview card is an accepted tradeoff, not an oversight — because Remy has no per-post landing page, every Facebook post links to the same canonical CTA, so every post's preview card looks identical (only the text varies). The owner explicitly weighed and accepted this for ~156 posts/year rather than waiting on a per-theme landing-page system.
  • Facebook shares one live Buffer channel across staging and production — the same exception already used for X/LinkedIn, since Buffer has no sandbox and the blast radius of a staging post is a deletable draft.
  • Instagram and a generated-image "Route B" are explicitly deferred to a single follow-up doc — Instagram's Buffer schema has no link-preview field at all, so it structurally requires a real hosted image; that work is scoped together with Route B rather than bolted onto this milestone. See docs/product/route-b-instagram-scope.md.
  • Marketing-site links only, never an in-app SPA route — the frontend has no per-route meta-tag mechanism and none was built for this milestone; Route A links only to marketing_landing pages.

Requirements Coverage

7 / 9 tracked scope-doc items satisfied (quoted from the MILESTONE-AUDIT). Two unmet items — the admin review UI (shipped, then deleted by an unrelated PR) and wiring the new 1200×630 image into the marketing page's og:image tags — are both small, well-scoped follow-ups; see Tech Debt below.

CategoryCountStatus
Backend delivery path (client, job guard, deploy wiring, tests)4All satisfied
Admin review surface1Not currently satisfied (shipped, then deleted by unrelated PR #1638/OBJ-1562 — OBJ-1576)
Prompt + measurement2All satisfied
Docs1Satisfied
Preview-card asset wiring1Deferred (asset shipped, HTML wiring did not — OBJ-1575)

Full detail: v4.9-facebook-social-publishing-MILESTONE-AUDIT.md on GitHub.

Outcomes

Facebook is now a live delivery channel for Remy's scheduled posts: she can draft a Facebook post with a link, an admin can approve it in the review queue, and it lands in Buffer as a scheduled draft — never auto-published, same as X and LinkedIn. Click-through is measurable through the existing weekly PostHog cadence via utm_source=facebook. As a side effect of Phase 0's fix, X and LinkedIn delivery — previously a silent no-op in every deployed environment — now actually works too, pending the next production release tag.

What is not yet true: a Facebook post approved today still previews with the old 390×844 portrait screenshot, not the new 1200×630 landscape card this milestone designed; and the admin reviewing that post sees a raw JSON dump of the artifact payload rather than the dedicated Facebook label, character counter, and safe clickable link-preview this milestone built (shipped in Phase 1c, then unintentionally deleted by an unrelated PR before this milestone closed). See Tech Debt.

Tech Debt

  • (Phase 1c) The Facebook-aware admin review UI (label, character counter, safe clickable link_url preview with an http(s)-only scheme allowlist) shipped in PR #1629, but was unintentionally deleted ~3 hours later by an unrelated route-cutover PR (#1638, OBJ-1562, "Live Ops Board becomes AI Workforce landing") that retired the entire ReviewQueueView.vue component in favor of FocusDetailOverlay.vue. The new component has no Facebook-specific handling — an admin reviewing a Facebook draft today sees a raw JSON dump of the artifact payload as inert text instead of a formatted, safety-guarded preview. Found during this milestone's close audit (Roy's review of the close PR). Tracked at OBJ-1576, assigned to Desi for a UI micro-spec against the new component, then Codi.
  • (Phase 1b / design) marketing_landing/index.html's og:image, og:image:width, and og:image:height meta tags still point at the old portrait screenshot instead of the new og-card.png shipped in PR #1615. The scope doc named this as Phase 1b's engineering work, but Phase 1b's shipped diff didn't include it, and no other phase picked it up — found during this milestone's close audit. Tracked at OBJ-1575.
  • (Phase 0, docs-adjacent) A code comment in rails_api/app/services/ai/social_publisher_client.rb (lines 144-148) still reads "UNVERIFIED... has not been confirmed against a live channel," contradicting Phase 0's own completed live-verify. Tracked at OBJ-1578 (→ Codi) rather than corrected directly (a Rails source file is outside this doc-focused close).
  • (Phase 0) The live Buffer test-draft post created during verification is still sitting in the Buffer UI — Josh to delete it manually when convenient (no Buffer credentials available in-runtime to do it programmatically).
  • (Phase 0) The production v* tag carrying this milestone's work (and #1605) is still pending the next weekly release train — until tagged, Facebook (and X/LinkedIn) delivery stays a no-op in production specifically; staging is already live.
  • (Milestone-adjacent) A ~15% chronic Playwright staging-smoke flake, unrelated to this milestone's diffs, surfaced during Phase 1a's release gate and is tracked separately at OBJ-1566 (assigned to Tess).
  • 71d7bc69d — Buffer GraphQL client rewrite (#1605)
  • 9f060a287 — Phase 0: human-gated Buffer verification tooling (#1616)
  • de6673520 — Phase 0 fix-round-2: correct Buffer post-check query (#1622)
  • b1c061f29 — Phase 1a: wire Facebook into the Buffer delivery path (#1630)
  • ad77eb1aa — Phase 1c: Facebook support in Review Queue, with link-scheme security fix (#1629)
  • 61df5b269 — Unrelated route cutover that deleted Phase 1c's review UI (#1638, OBJ-1562)
  • 547952e8e — Phase 1b: Remy prompt + UTM measurement, docs landed in-PR (#1639)
  • 7c9790186 — Design: 1200×630 landscape og:image social card (#1615)
  • 7f852580e — Beat taxonomy: five-beat Content Beat Framework adoption (#1614)

Last updated: 2026-07-21

Loading…