You pick the brand, the feature, the channels and the shape. Those picks become the prompt that writes the post. The whole journey, one small step at a time.

Social makes a post. You pick a few things — the brand, the feature, the channels, the shape — and say the topic in a few words. Those picks are not a form: each one is a paragraph of context handed to the writer. Claude writes it in Arabic and English, a craft check reads it before you do, and nothing goes out until you say so.

The four pages

/social is one job split across pages, and /social/publish can do the whole job by itself — which is why the bare /social opens there. The other three exist for when you want to start somewhere earlier.

Nothing is lost moving between them: the copy you are working on, the pictures you have attached, and every one of those settings follow you from page to page. Pick a brand on Draft and Publish is already publishing as that brand.

The settings each page shows

One row, one set of answers — but a page only shows the words that mean something where you are standing. A stage that cannot send has nothing to schedule; a stage that only finds pictures has no post shape to pick.

WordcalendardraftmediapublishWhy
Brand●●●●Everything is per-brand: whose topics, whose voice, whose library, whose Page.
Feature●●●●Which part of the product. It narrows the topics, grounds the writing, and filters the library.
Channel—●—●Where it lands. It changes what gets written, so Draft needs it; nothing on Media or Calendar turns on it.
Post—●—●The shape — words, one image, a carousel, a reel. It changes what the writer is told to write.
Draft—●—●On Publish, the drafts already waiting. On Draft, its own three: angle, register, model. Same seat, different question.
Media—●●●Which pictures are in play. On Media it also narrows the results, and the count on the bar says by how much.
Queue○——●What is already asked for and unanswered. Calendar shows this per brief as a chip, so it belongs — it is not wired into the row yet.
Plan———●Now, later, or out for approval. Only a stage that can send has any use for it.

● shown · ○ belongs, not wired yet · — deliberately absent

Draft and Media grew this row on 2026-08-26; before that Draft's seat held three writing knobs and no brand, and Media's seat did not exist — so a person asking for copy had to leave the box they were writing in to say which brand it was for. Calendar is the one page still without it: Brand is currently a page-level context rather than a word in the row, and Queue would earn its place there the moment it is.

PageWhat you go there for
calendarThe topics we already decided to write about this week, per brand, with the ones due now highlighted. Press Queue now and it becomes a request for words.
draftThe place to ask for copy in a sentence and get Arabic and English back. Type again to refine what is on screen rather than starting over. Use Arabic carries it to publish.
mediaType two words and the library narrows; Attach puts a picture in the tray. Generating something new lives below it — this page finds what exists, the studio asks for what does not.
publishThe whole job in one place: write it, refine it, attach, and send — now, later, or out for someone else's approval. It is also the review queue, where a draft someone else asked for waits for a yes.
measureThe last twenty posts, with what each one reached.

How a post travels

The fourteen steps below all happen on /social/publish — this is that one page, opened up. Eight of them are yours — 1 to 4, then 8 to 11 — and every one of those is a tap or a few words.

What each step means

1 · Open the box. /social opens on Publish, because it is the only stage that can finish a post on its own. One glass bar: the field, a ⊕ on one side, a seat on the other.

2 · Set the post. One row behind the seat, four answers, and none of them a form field — each is a paragraph of context handed to the writer.

  • Brand · Balqalam, Mkan, databayt, Sijillee, Moallimee. (Hogwarts writes through Balqalam, its Arabic face, so it is not in the picker.) The biggest single pick — see why below.
  • Feature · which part of the product the post is about: Admission, Attendance, Exams, Grades… That brand's own block registry, narrowed to the blocks a post can be about — twelve for Balqalam, six for Mkan, four for databayt, none yet for Sijillee or Moallimee. Optional, and it is the difference between a post about a school system and a post about attendance in one.
  • Channel · where it is going. A channel is offered only when both the transport and that brand's own destination are wired, so an unpublishable channel is never on the list.
  • Post · the shape. Words alone · a caption beside one image · one caption over several · a video · an image or video with no words · a swipe deck · a story · a reel. Nine of them, and each changes what the writer is told to write.

They stay set between visits and follow you across the stages, so this is a step you do once and then rarely touch — which is why it is one box and not four.

3 · Say the topic. A few words is enough — "attendance". Or press one of the week's ready briefs and type nothing at all: content/social/pillars.json holds each brand's recurring topics with the audience, the call to action and the constraint already written. The Calendar stage is that same file, shown as a week.

4 · Press Write it. The seat on the right of the bar. That is the last thing you do until there is a draft to read.

5 · Every pick becomes a line of the prompt. This step is the whole point of the pipeline, and it is spelled out below.

6 · Claude writes it. Arabic written first and English mirrored from it — a sibling, not a translation. About eleven seconds when the inline lane answers. When it cannot, the same ask falls to a queue that a Claude Code session drains, and the box tells you which of the two happened instead of spinning.

7 · A craft check reads it first. It refuses on its own — it is not advice. A banned word, a link that would publish untracked, a post too short to say anything: the draft goes back to be written again rather than forward to you, and the second attempt is told which rules it broke by name. The very first Balqalam draft was refused for writing balqalam.com without https://, which would have shipped with no way to count it.

8 · You read it. The copy lands in the same field you typed the topic into.

9 · Say what to change. Eighty percent right is the normal case, and it used to mean retyping the whole thing. Say "shorter, drop the parent alert" instead: the same draft is rewritten rather than replaced, so the queue shows one current draft and not three versions competing for the slot. Measured: 24 seconds.

10 · Attach a photo or a video. Optional. ⊕ opens the library — pick something that already exists, or ask /higgs for a photo or a video and /carousel for slides with words on them. One photo, up to ten as an album, or one video.

11 · Say when. Now sends it. Later books a time, and a sweep every ~15 minutes publishes it when the time comes. For review makes one signed link per channel that anyone can approve from a phone — one click publishes, and a second click gets already handled rather than posting twice.

12 · It posts to your channels. One press fans out to all of them. Today that means Facebook, for three of the five brands — the rest are wired in the data and deliberately not built yet (Status).

13 · It saves the receipt. The platform's own post id, so the post can be taken down again. Every link is tagged so its clicks can be counted.

14 · It reads the numbers back. Reach and engagement, every six hours.

The rules every post obeys are in Brand & Voice; the build roadmap and credential plan stay internal (docs/SOCIAL-AUTOMATION.md).

The other way a post travels

Everything above is you driving it — you open the box, you say the topic, you press Write it. There is a second path where nobody opens anything: the week runs on timers, and you only say yes.

Seven steps instead of fourteen, and you touch exactly one of them.

Both paths end in the same place, and they share the same queue — a post seeded on Monday and a post you typed yourself both wait on /social/publish for the same yes.

What happens when nobody says yes

Step 4 is the only box with no timer on it, which makes it the only place the week can stop. It already did, for 27 days.

Nothing broke. Every job ran and honestly reported success, because each one truthfully emptied a queue that nothing had reached. Green meant "nothing to do", not "it worked." A daily nudge now says what is waiting, and stays quiet when nothing is.

The loop → — what each box does, and the one thing to get right when you approve.

Why this beats writing it yourself

Not speed, and not the login.

Ask a good copywriter for "a post about Mkan" and they will not start writing. They will start asking:

  • What is Mkan? Who reads this — renters, or hosts?
  • What is the tone? What must it never say?
  • Which part of the product is this about?
  • Where is it going? A Facebook caption is not a story.
  • What shape is it — a caption, a swipe deck, a reel?
  • What did we post before this, and what comes after?
  • What time of year is it for the reader?
  • What have we tried that did not work?
  • How will we know whether it worked?

A brief that answers none of those gets a post that says only a topic. That interview is the real work, and doing it properly every single time is exactly what nobody has the patience for — which is why posts written by hand get shorter interviews as the week goes on, and worse as a direct result.

The pipeline's job is to have the answers ready before the question is asked. Most come from registries the engine already keeps; the rest come from four taps. Where an answer does not exist, the question is left out of the prompt rather than answered vaguely.

The question a copywriter would askWho answers it here
What is it, and who reads it?the brand pick
What must it never say?the brand row, plus the craft gate
Which part of the product?the feature pick
Where is it going?the channel pick — named today, not yet written for
What shape?the post shape pick
What time of year?the calendar, on its own
What have we tried that failed?the last twenty dismissals
How will we know?the receipt and the six-hourly numbers
What came before and after?nobody yet — see below

Read the other way round, the picks are a prompt template being filled in. Every setting has a clause waiting for it, and picking swaps a generic clause for a specific one. Sections you leave unpicked are left out entirely rather than rendered empty — so an ask with no direction looks like an ask with no direction, instead of quietly reading as a direction to be generic.

Here is the same topic, asked both ways.

Nothing picked:

You are Databayt's lead social media writer for MENA.
Product: balqalam — SaaS product by Databayt.

House Rules: …

User Brief: attendance
Angle directive: Choose the best angle (pain, moment, or proof)
Arabic Register directive: Rung 2

Brand · feature · shape · channels · media · angle · register picked — the same three seconds of tapping:

You are Databayt's lead social media writer for MENA.
Product: balqalam (بالقلم) — the Arabic-facing face of the same school management
SaaS: admission, attendance, timetable, exams, grades and fees in one system instead
of five notebooks and a WhatsApp group. Audience: school owners and principals in
Saudi Arabia and the wider Arabic MENA market — administrators, not technologists —
with teachers and parents as the daily users. Never carries the Hogwarts wordmark.

Scenes from the reader's own week — ground the post in one it can honestly claim:
- <this month's moments, from the school year's current window>

House Rules: …

User Brief: attendance
Feature: Attendance — the part of the product this post is about.
Shape: a swipe deck. The first panel is the hook and has to stand alone; the caption
underneath is short.
Going to: Facebook, Instagram.
One asset is already attached — write copy that sits beside it without narrating it.
Angle directive: pain
Arabic Register directive: Rung 3 (pan-Arab colloquial)
Reviewers recently dismissed drafts for: hook 3×, two-posts 1× — these are habits to
break, not suggestions.

What each pick is worth

The brand is the largest one. It carries what the product is, who reads it, and what it must never say — Balqalam's row ends "Never carries the Hogwarts wordmark." One row per brand, and a test pins the map to the product registry so the next brand cannot arrive silently. It matters because it already failed once: Balqalam had no row until 2026-08-25, so every draft ever written for the Hub's default brand came from a writer who did not know it was school software. Asked for "attendance", it produced clean, on-register Arabic about tracking employees.

The feature becomes "Feature: Attendance — the part of the product this post is about." The list is not invented for social: it is each product's own block registry, narrowed to what a post can be about. Infrastructure is deliberately absent — nobody writes a post about a sidebar.

The post shape becomes a writing instruction rather than a label. A card reading "text + image" tells a writer nothing; a caption under one still is a different length from a swipe deck's opening panel, so the shape is read out as the difference:

  • swipe deck → "the first panel is the hook and has to stand alone; the caption underneath is short."
  • reel → "the hook is spoken in the first seconds, so the caption adds rather than repeats."
  • story → "a tall frame — one thought, a handful of words, read in passing."
  • gallery → "one caption over several images. It carries the whole set, not the first picture."
  • words alone → "nothing rides with them, so the copy carries the image itself."
  • image-only → "write nothing longer than a line, and only if it adds something the image cannot say."

The channels become "Going to: Facebook, Instagram."

Media already attached becomes "write copy that sits beside it without narrating it" — because the reader is already looking at it. With nothing attached the clause flips to "Suggested visual: …" instead: with assets in hand the register is already decided, and asking for one would describe a picture nobody is going to make.

The angle is pain, moment, or proof. The register is a rung: 2 simplified MSA, 3 pan-Arab colloquial, 4 local dialect.

The month you are in is a pick you never make. content/social/scenes.json carries each brand's year as windows — year-start, mid-term, term-close, Ramadan revision, certificate exams — and the current one's moments are handed over as scenes the post can honestly claim. Documents contain no Thursdays; these do.

What reviewers dismissed recently rides too — "hook 3×, two-posts 1× — these are habits to break, not suggestions" — so the writer is corrected by the last twenty drafts, not just by the rules.

And the house rules never leave: 400–900 Arabic characters, matched to the craft gate's own floor so the prompt cannot steer a draft into the refusal band; Latin digits, never Arabic-Indic; at most one emoji and never in the first line; no exclamation marks; the English a sibling of the Arabic rather than a translation; never open with the brand name or "We're excited to announce"; one idea per post, because a bulleted feature list is two posts; and never invent a number, name, price or date that is not in the brief.

Even the order is engineered: static sections first (role, brand, scenes, house rules), per-ask ones last (brief, instruction, direction, lessons, violations), because the model's implicit caching keys on an identical prefix — measured at ~65% token reuse. Reordering a section is a cache invalidation, not a style edit.

What a person cannot do

A person writing by hand has every one of these available and none of them in front of them. That is the whole gap:

Brand knowledge lives in whoever is typing, and leaves when they do. Here it reaches the writer before it writes. Refusal does not exist by hand — nothing stops a boring or untracked post. The craft gate rejects on its own. Memory does not exist either: a post typed by hand leaves nothing behind. Here the platform id is saved so it can be retracted, links are tagged at delivery, metrics are read six-hourly, and dismissals feed the next draft. Delegation by hand means sharing the account. Here approval is a conditional claim, so two reviewers cannot double-post, and a signed single-use link approves from a phone with no token in anyone's hands.

Measured 2026-08-25, on the Balqalam Page's own brand: brand + feature picked, nothing typed, publishable Arabic in ~25 seconds. Then one sentence — shorter, drop any out-of-app notification — and 24 seconds later the draft no longer promised a parent alert that balqalam.mdx forbids in as many words.

Where it does not beat manual yet

Stated plainly, because the argument above is only worth what is true.

The channel is named to the writer, not written for. "Going to: Facebook, Instagram" is the whole of it today — one post in one voice goes to every selected channel. Per-channel rewriting is real in the schema (SocialVariant.text is per-channel) and deliberately unbuilt under the one-slice decision. That is the next multiplier, not a shipped one.

Reach is one channel wide. One press already fans to every selected channel, but only Facebook is live, for three of five brands — so today's win is quality and memory per post, not reach per press.

The writer has no memory of the plan. It knows what reviewers rejected lately, but not what actually went out last week or what is booked for next — so it cannot avoid repeating itself, or deliberately follow on from the last post. The plan exists and you can see it (pillars.json, rendered as the week on the Calendar stage), and the ledger exists on the Measure stage; neither reaches the prompt. Of the copywriter's questions above, this is the one still unanswered, and it is the cheapest one left.

The feature is matched on words, not stored. Nothing records which feature a post sold; featureFits narrows a list by looking for the word. And copy typed by hand into the field is not craft-gated, and dismiss has no button — see Status.

Four doors open onto Send to the platform, and they differ only in who says yes: the review queue on /social/publish (a person clicks Approve), a signed link in the private Slack review channel (a person clicks once — a second click gets 409 Already handled), the drain (a post booked for later, checked every ~15 minutes), and the relay (no person; a machine with the cron secret). All four funnel into one shared egress path — see Facebook for that path step by step.

Sending it out to a Facebook Page

The step above marked Send to the platform, opened up. Every door — review queue, signed link, drain, relay — arrives here, at deliverPost (src/lib/social-publish.ts) and then sendFacebookPost (src/lib/facebook.ts).

Nothing in this path knows a brand by name. It builds the setting name from the brand id, which is why adding a Page is configuration and never transport code.

Where an approved post comes from

Those four boxes are the only four things in the codebase that call deliverPost. They differ in exactly one way — who is trusted to say yes:

DoorEntry pointWho may open it
Review queueapproveDraft (src/actions/post-social.ts)A signed-in contributor. requireContributor() re-checks the session email against the allowlist on every call
Signed linkPOST /api/social/publishAnyone holding the link — its only protection is being unguessable, which is why the review channel is private. Approving is a one-way status change, so a second click gets 409
DrainGET /api/social/drainThe CRON_SECRET bearer. Publishes variants whose scheduledFor has passed — those were approved earlier, in schedule mode
RelayPOST /api/social/relayThe CRON_SECRET bearer. No human at all — the headless lane

The words arrive one of three ways: a contributor files a brief on /social and a Claude Code session drains the queue and writes the copy back through the craft gate (scripts/social-drafts.mjs answer); a reviewer edits copy directly in the queue's editor, which never creates from blank; or — since 2026-08-25 — the composer asks for itself. Its Write it button turns the field plus every setting above it (brand, feature, post shape, channels, attached media, and the angle/register/model knobs) into a brief, files it with requestSocialDraft, and drops the answer back into the same field. The inline Gemini lane answers in about eleven seconds; a miss falls through to the same queue and the same drain, and the box says so rather than spinning. The answered request becomes the composer's active draft, so approving there consumes it instead of leaving it approvable twice.

The media is attached, never invented at send time: library assets picked by URL (content/media/library.json, or the showroom's Attach), landing on SocialDraftRequest.mediaUrls and then SocialVariant.mediaUrls. deliverPost receives URLs and nothing else — Graph fetches the bytes itself, which is why they must already be public on the CDN.

A fifth door opened 2026-08-25: the composer itself. publishPostDirect and schedulePost used to be a publish path with nothing driving it — no route, no component. The review queue's composer now calls both: copy typed into the box that answers to no queue entry publishes through publishPostDirect (Now) or schedulePost (Later, for the time the Plan tab picks). Copy that came from a draft still goes through approveDraft, because approving is also a claim. The door count is five; who may open this one is the same signed-in contributor requireContributor() checks on every other call.

The saved id is the only way back. Graph returns it once, in its reply; it cannot be recovered from the variant afterwards, and it is the sole argument deleteFacebookPost accepts. A test post that was never recorded is a test post the system cannot retract. sendFacebookPost deliberately prefers post_id over id, because on a photo post the bare id addresses the photo rather than the post.

Both Arabic-facing Pages were run through this whole path on 2026-08-24 — posted, read back, retracted, confirmed gone. Neither had ever carried a post before that. The per-brand ids, tokens and traps live on Facebook.

Business models → who we're talking to

databayt runs on four business models. Social marketing serves each one differently — B2B storytelling for the first three, B2C reach for the SaaS products.

ModelTypeWhat it sellsSocial job
Per ProjectB2BCustom automation builds for partnersShow the work — case studies, "automate the boring" proof
PartnersB2BOutsourced product dev + specialized talentAttract teams & talent — founder story, capability posts
CodebaseB2B / B2CReusable components, contributor revenue-share (the sharing economy)Grow the contributor community — open-source, dev-rel
SaaSB2C / B2BReady-to-deploy products (Hogwarts, Mkan, Souq, Shifa, …)Sell the product — per-brand consumer & buyer marketing

The first three are the databayt umbrella story (B2B, developer- and partner-facing). The fourth is where the product brands live — each with its own audience, channel mix, and voice.

Strategy

How we actually win is its own page — Strategy: six lenses (organic · viral · when · grow · pay off · moral), the create-once-repurpose-everywhere multiplier that makes 5 brands × 8 channels feasible for a small team, per-brand cadence, and the kill criteria that keep social honest against the cash-flow-first drive.

Brand pages

Every brand has its own presence. Its audience, channel mix, content pillars, and account status live on its page:

  • Databayt — the umbrella: open source, sharing economy, Kun & dev-rel (B2B)
  • Hogwarts — school management SaaS (B2B schools + B2C parents)
  • Mkan — property rentals (B2C renters + B2B hosts)
  • Moallimee — teachers & tutoring (pre-launch)
  • Sijillee — Sudan business accounting (pre-launch) (B2B)

Souq and Shifa are live SaaS products but are not in the current priority-5 social push. When they join, they get a page on this same template.

The pipeline

The stage-by-stage view of the same journey diagrammed in How a post travels — who owns each step, and what it actually does.

A full draft is copy AND/OR media — text · text + image(s) · text + video · image(s) only · video only. The stages are wired to each other: pillars seed the draft queue, media attaches to the draft (SocialDraftRequest.mediaUrls), and the answered full draft queues for a human on /social/publish.

StageOwnerNotes
Plancalendar skill · weekly cadence · growth agent pillarsWhat to say, to whom, when. content/social/pillars.json IS the recurring calendar — the Monday seeder rotates it into the draft queue, and /social/calendar renders it with a Queue now button
Draftdraft skill — Claude, natively — the agent window on /social/draft, or the composer itself on /social/publishArabic-first bilingual copy against Brand & Voice, plus the media half. Three askers, one queue: all of them file a SocialDraftRequest, the inline Gemini lane answers in ~11s where it can, and a Claude Code session on the Max pool answers the rest — the API lane of D-20260730 has no funded key, so drafting stays on the subscription. Contributor-gated
Mediamedia keyword → /higgs (text-free) · /carousel (text-bearing)Pick before you generate: library.json by brand + type, or the brief's (library: id) hint. New renders register in the library, then attach to the draft (social-drafts.mjs attach, or the showroom's Attach)
Approveapprove skill → human (Abdout / Ali / Samia)The gate — the review queue on /social/publish is the primary lane; a signed 12h link covers a remote approver. See Autonomy ladder
ScheduleSocialVariant.scheduledFor + /api/social/drainThe review queue's approve-mode setting: publish right away, or schedule and delegate to the drain, which publishes due variants every ~15 minutes from GitHub Actions. No keyword of its own — it is publish's second verb ("schedule it for tomorrow")
Publishpublish skill → egress layer — Telegram + Facebook direct APIs · Hermes-transport channels via the gateway · WhatsApp as a copy-out blockEgress only — dumb relays, never the LLM. Media routes by shape: text · one photo · 2–10 album/carousel · one video; mixed kinds are refused by name
Measuremeasure skill → /api/social/metrics → SocialMetricFacebook reach + engagement, six-hourly. UTM tags are applied but not yet consumed — no PostHog in the app

Each stage is also its own keyword: say "draft a post for hogwarts" and only that stage runs. social is the orchestrator that chains all six.

The Hub is this table, as routes

/social mirrors the pipeline: seven stages across five routes (/social/<stage>), in order, with brand · channels · status above them as context that spans all of them. Each stage is a page — linkable, shareable — and the bare /social opens on Publish, which since 2026-08-25 is the only stage that can finish a post on its own: ask for the copy, refine it, attach the media, and send.

RouteStagesWhat is there
/social/calendarPlanThe same box, searching the plan: type two words and the briefs narrow, and pressing one files it as a draft ask. This ISO week's rotation picks sort first (the same math the Monday seeder runs). The full plan stays underneath — every brief in the file's own order, a queue chip on each (in the queue · answered · approved · dismissed) and Queue now on the ones nobody has asked for
/social/draftDraftThe agent window, and since 2026-08-25 the same box the other stages wear: one glass bar whose field is a BRIEF, its ⊕ the shared media tray, its empty seat Publish's settings row — Brand · Feature · Channels · Post · Direction · Media, every one of which changes what gets written. Timing and Queue are absent: this stage cannot send. Typing again refines the draft on screen rather than starting another. The answer, its media and the "Use …" buttons sit under it — "Use English" is the only place the English half is reachable
/social/mediaMediaThe same box, finding rather than asking: type two words and the library narrows, and each row attaches into the shared tray. Its seat carries the settings row too — Brand · Feature · Media — which narrow the results, with the bar's count saying by how much. The showroom below it still browses — shelves by brand, cards with model, credits and source. Media Studio (generation) sits under the gallery: the box finds what exists, the studio asks for what does not
/social/publishDraft · Media · Approve · Schedule · PublishWhere the bare /social opens, because it is the only stage that finishes a post on its own. One box: the field is both the post and the queue search, ⊕ attaches, and the seat sends. Behind it, eight words — Brand · Feature · Channel · Post · Draft · Media · Queue · Plan. Draft carries the plan's briefs as one-press starts, the angle/register/model knobs, Write it, and Refine. Plan is what Send does: Now, Later (with a time), or Review (one signed link per channel). A draft loaded from Queue is approved rather than re-published, which claims it answered → consumed
/social/measureMeasureThe same box, searching the ledger: type two words — a phrase from the copy, a channel, or a status — and the row is there with its channel, status, time and reach on it. Nothing is pressable, because a variant is a record of a decision already taken. The last 20 variants stay as a table underneath

Cross-stage state lives in the layout's provider, which persists across stage navigations: the agent's queue poll runs on for minutes, the editor keeps its copy, the post's settings — brand, feature, channels, shape, media filters — are the same on every stage, and the attachment tray survives a trip to the showroom and back — switching stages must not throw any of it away. Taking a draft ("Use Arabic") moves the Hub to Publish carrying copy, media, and the request id, so approving there consumes the queue entry instead of leaving it approvable twice.

Drafting from the Hub — the two-sided loop

The agent window is how a teammate without a Claude Code session gets copy on the Max subscription. It has two halves and both have to run:

WhoWhereDoes
Any contributor/social → DraftPicks the brand, writes a brief, presses send. The ask becomes a SocialDraftRequest and the window polls
Any Claude Code sessionthe repoSays draft with no topic → queue mode drains the pending asks through the same brand-voice doctrine, and writes the copy back
node scripts/social-drafts.mjs list                                    # what is waiting (incl. mediaUrls)
node scripts/social-drafts.mjs answer <id> --ar ar.txt --en en.txt \
  --media "https://cdn…/hero.png"                                     # the copy + its media half
node scripts/social-drafts.mjs attach <id> --media "url1,url2"         # media alone, after a render
node scripts/social-drafts.mjs fail   <id> --note "what is missing"    # shown to the asker

The brief is the whole input. The answering session receives the brand and the brief and nothing else, so a brief that names only a topic gets copy that says only a topic. Name the fact, the audience, the call to action, and any date, name or link that must appear. fail --note is rendered to the asker verbatim — use it to ask for the missing piece rather than inventing one.

The queue drains on a schedule: scripts/drain-drafts.sh runs every 5 minutes from launchd on a teammate's Mac, beats a draft-drain heartbeat on every look (even when empty), and only invokes Claude when asks are pending — zero tokens idle. The window shows the queue's truth while waiting (position + last check), backs off its poll, and stops after 10 minutes into a "still queued" state; the drain sweep expires asks nobody answered within an hour — see Current status.

Which of the three loops a post travels — Hub, cron draft-and-approve, or the inbound Hermes relay — plus the actor map, the cron inventory, and the keyword table, are on Architecture & Loops.

Layer boundaries

The one rule that keeps the architecture honest: Hermes is a broadcast relay, not a brain. Anthropic offers no native "post to X" primitive, so a self-hosted last-mile gateway is a legitimate egress — but the writing stays Claude-native. This is what reconciles a self-hosted gateway with the "anthropic-native over self-hosted" doctrine.

ConcernLayerWhy here
Write the copyClaude (in Claude Code / Cowork)Best model, brand-aware, bilingual — never Hermes' LLM
Make the image/video/higgs → Higgsfield CLIAlready governed by budget + brand kit
Decide it's goodHuman (→ automated gate at L4)Brand accounts are public and irreversible
Reach the platformEgress layer, routed per transportTelegram and Facebook: direct APIs (plain HTTPS — works from Vercel), drained by kun. Other channels via the Hermes gateway, which pulls its own work. WhatsApp: no API exists, so a copy-out block
Prove it worked/api/social/metrics + UTMFacebook reach and engagement into SocialMetric; UTM tags attribute traffic at our own site

Note what is not in that table: Slack. It is where approvals and notices are relayed, which is a different job from reaching an audience — see the channel model.

Done (Phase 2, 2026-07-10). The old generateAndPublishPost action drafted via the gateway's own LLM and never published — a name that lied twice. Rather than rename it, Phase 2 removed the in-app draft lane entirely: drafting is Claude-native via the /social skill, and the hub is compose → stage → publish only.

The eight channels

The full set: Facebook, Instagram, Telegram, TikTok, Snapchat, X, LinkedIn, WhatsApp. Each has a very different posting-API reality — some trivial, some paid, some with no organic-post API at all. Build order follows feasibility, not wish-list:

ChannelPosting APIEffortNote
TelegramBot APITrivialWired direct (2026-07-10) but not configured — the token was never pushed to Vercel and no TELEGRAM_CHANNEL_ID exists. Two variables from live
XAPI v2Easy (paid)Pay-per-use since Feb 2026 (~$0.015/post) → /decide
FacebookGraph APIMediumThe one live channel — hogwarts, mkan, databayt
InstagramGraph APIMediumBusiness account linked to a FB Page
LinkedInPosts APIMediumCommunity Management API approval + page admin
TikTokContent Posting APIHardApp audit (~1–4 wks); posts private until passed
SnapchatPublic Profile APILimitedAllowlist-only via a Snap contact
WhatsApp(none, ever)ManualNo organic posting API for Channels or broadcast lists — not a gate to pass, a capability Meta does not offer. Modelled as transport: "manual": drafted, rendered, and approved like any channel, then handed out as a copy-out block for a human to forward

Per-channel state — wired vs configured vs live — is on Status, which is the page to trust when the two disagree.

And one channel that is not one of them

Slack is the communication channel, not a distribution channel. It is the team surface where approvals and notices land, and it is deliberately absent from the table above: counting an internal chat as audience reach is how "1 of 9 channels live" came to describe a system with one live audience channel. The composer does not offer it, the write gate rejects it, and /api/social/relay refuses it by name. Nothing needs selecting — sendReview routes there unconditionally.

The registry expresses this directly: a channel carries a kind (distribution | communication) that says what it is for, orthogonal to the transport that says how its bytes move.

Full matrix, costs, and the aggregator option (Ayrshare covers 7 of 8) are in the internal roadmap. Per-brand channel priorities are on each brand page.

Autonomy ladder

The north star is fully autonomous publishing. We climb to it in stages — not because autonomy is hard to code, but because handing a public brand account to an unsupervised bot needs guardrails first. We are at L2 on Facebook — the one live channel: compose or schedule in the Hub, one-click publish from a signed approval link, drain delivers — and L1 elsewhere. (This previously read "L2 on Telegram", which had the ladder pointing at the channel that cannot publish at all rather than the one that can.)

LevelWhat runsHuman role
L0 — ManualHuman writes and postsEverything
L1 — AssistedClaude drafts + /higgs mediaHuman posts manually
L2 — Stagedkun stages copy+media to the dashboardOne-click approve → publish
L3 — Semi-autoAuto-publish low-risk channels (Telegram); gate brand channelsApproves brand posts only
L4 — Full-autoAutonomous end-to-endNone — replaced by an automated gate

L4 is gated behind /decide + credentials + an automated guardrail layer: an LLM-judge content + Arabic-correctness check, per-channel rate limits, and a kill switch. Autonomy without that layer is not a feature — it's an incident waiting to happen.

The keywords

Six stage keywords — the pipeline table's seven rows fold onto six words because Schedule rides publish. Each runs on its own; social chains all of them, and carousel is the multi-slide sibling with its own chapter.

KeywordStageSay
calendarPlan"what are we publishing next week"
draftWrite"draft a post for hogwarts about admission"
higgsMedia"make the image for it"
approveGate"send it for review"
publishDeliver + schedule"publish it" · "schedule it for tomorrow"
measureRead back"how did the post do"
socialAll six"social post for mkan about the new listings"
carouselMulti-slide"carousel for hogwarts intro"

They were split out (2026-07-27) because the pipeline above had named these stages and assigned them owners for months while four of them had no keyword — and Abdout types prose, not slash commands, so a stage with no name is a stage that cannot be reached. Each skill's when_to_use carries an explicit negative clause naming its siblings, which is what stops "publish it" from routing to /ship.

The growth agent owns the strategy layer above them; .claude/vocabulary.json holds them all in the Pensieve school. No slash needed — say the word.

Current status

The Social Hub at /social is multi-product (Phase 3, 2026-07-23):

  • Works — bilingual (en/ar, RTL) composer with a config-driven channel registry (8 distribution channels, plus Slack as the one communication channel, which the picker does not offer) × a product registry (hogwarts · mkan · databayt · sijillee · moallimee). A product select — same visual language as the shadcn theme selector — scopes the whole page: channel toggles, health checks, and publish all carry the chosen brand.
  • Per-product Facebook — each brand has its own Page and its own permanent Page token, read from FACEBOOK_PAGE_ID_<PRODUCT> / FACEBOOK_PAGE_ACCESS_TOKEN_<PRODUCT>. The Egress panel names the Page the token resolved to, which is how you catch a crossed token before publishing. Live for hogwarts, mkan, and databayt; sijillee and moallimee have no Page yet.
  • Per-product wiring — a channel is publishable only when the global transport and that brand's own destination are wired (productChannelWired). Telegram stays databayt-only: there is exactly one Telegram channel, org-level, so claiming it for a product brand would post "as Hogwarts" into the org channel. Slack is excluded structurally rather than per-brand — it is a communication channel, and productChannelWired refuses it for every product.
  • Auto-post is draft-and-approve — a daily Vercel cron (/api/social/cron, CRON_SECRET-guarded) drafts per opted-in product and sends the draft to a private review channel with a signed, 12-hour, single-use Publish link. The link opens a read-only confirm page and only its POSTed button publishes — a chat client's link-preview crawler can never consume an approval. Nothing publishes unattended. Opt in per product with SOCIAL_AUTOPOST_PRODUCTS; empty (the default) means the cron does nothing.
  • Inbound relay — because Vercel can never reach Hermes on localhost, /api/social/relay inverts the arrow: Hermes drafts, a human approves in Slack, Hermes POSTs the approved text to kun, and kun delivers with its own tokens. Hermes never holds a Page token.
  • Drafting stays out of the relays — SOCIAL_DRAFT_SOURCE=hermes (default) hands the ask to Hermes; =anthropic drafts server-side via the API and is opt-in because the engine's billing posture is subscription-only.
  • Media is a set, and it rides the whole chain — a draft carries mediaUrls (≤10 public URLs, typically /higgs or /carousel renders on the CDN) from the ask through approval to delivery, which routes by shape: text · one photo (/photos edge, sendPhoto) · 2–10 as a Facebook carousel or a Telegram album (sendMediaGroup) · one video (/videos, sendVideo). Over-long copy spills into a follow-up message rather than being truncated. Mixed image+video, two videos, or more than ten images are refused by name before anything is delivered, because no platform edge carries them. The confirm page behind a signed link lists every URL.
  • Multi-channel, with per-channel truth — the picker selects any subset of a brand's wired channels, the three transports fan out in parallel, and the result names each channel individually. A half-landed post no longer reads as one flat failure.
  • Stage for review from the Hub — the review editor can push the draft down the cron's approval path (signed 12h link into the private review channel) instead of deciding in-app, for when someone else signs off. Staging retires the queue entry, so a draft cannot be approved through both lanes.
  • Scheduling and the drain — the review queue's approve-mode setting (publish right away, or schedule) decides what Approve does; scheduled variants land as scheduled and /api/social/drain publishes them from GitHub Actions every ~15 minutes, claiming each row before delivering so two overlapping runs cannot double-post. Its channel filter is an allow-list, so a transport added later waits visibly in the queue rather than being attempted and silently lost.
  • Metrics — /api/social/metrics reads Facebook reach and engagement back into SocialMetric six-hourly. A missing scope or a retired metric name is terminal on the first attempt with a named remedy, rather than retried forever.
  • Ledger — /social renders the last 20 variants server-side (brand, channel, copy, status, time, latest reach/views), so pending approvals, scheduled posts, and publishes are visible without SQL.
  • The Social Agent window (2026-07-30) — the Hub's Draft tab: describe the post and Claude drafts it, Arabic first with its English sibling; one press moves the Hub to Publish with the copy in the editor. It is the same ai block as the sales agent — atom/agent-heading, atom/prompt-input, atom/ai-response-display, ported from hogwarts — so the two windows read as one product. The prompt names the selected brand, and says what a good brief contains, because the brand and the brief are the only things the answering session receives. Asynchronous by design. The window records a SocialDraftRequest and polls; a Claude Code session on the Max pool answers it through draft's queue mode. The API lane of .claude/memory/decisions/2026-07-30-in-app-draft-spend.md shipped but is blocked — no funded key exists behind ANTHROPIC_API_KEY — so the queue is the live path, and the reasoning panel describes that queue rather than performing invented thinking. This is not the old gateway-LLM draft card coming back: that card was removed (2026-07-23) because a relay was writing copy; here the writer is Claude itself.
  • The composer is an editor (2026-07-30) — the Publish tab wears the agent's shape: the same band, heading and rounded surface, sized for writing. Media URL and schedule collapsed from stacked labelled inputs into two popover pills, so the copy is the only thing on the page with weight.
  • Publish became a review queue (2026-08-05) — the blank-slate composer is gone. Answered full drafts (copy AND/OR media) queue on Publish oldest-first: the next one up is highlighted, every upcoming one is browsable, and loading one opens the editor prefilled with its copy and its attachments. The only actions are decisions — Approve (now or scheduled, per a settings popover), Dismiss, or send the signed review link — and an empty queue points back at Draft rather than offering a textarea. Approval claims the request (answered → consumed) with the same conditional update the drain uses, so two reviewers race safely; a total delivery failure hands the draft back with the reason, while a partial one stays consumed because re-approving would double-post the channels that landed.
  • The publish queue got a mode row (2026-08-25) — the search bar over the queue used to see only a third of the lane: listAnsweredDrafts reads work awaiting a decision, and work already decided lives in SocialVariant, so "did that one go out?" meant leaving the stage for the ledger on Measure. listQueuedVariants reads the other two thirds, and the bar wears hogwarts' category buttons as four split icons — All · Drafts · Scheduled · Published — each carrying the count of what a search actually landed on rather than the size of its table. A filter menu holds the knobs that are a preference rather than a slice (brand scope, order, media-only), and every row carries its kind's icon, its channel and its time. Only a draft loads into the composer: a variant is downstream of a SocialPiece, so approving one again would mint a duplicate — those rows open the ledger and say so on the row. The queue header's count and Refresh moved into the bar rather than standing beside it twice
  • Media is wired to the draft (2026-08-05) — the showroom's Attach fills a provider-owned tray shared by the draft ask, the review editor, and the showroom itself; the drain's answering session picks assets from library.json by the brief's (library: id) hint or brand + type and passes them with answer --media; /higgs and /carousel end by attaching their render to the ask it answers. Copy-and-paste-a-URL is no longer the hand-off.
  • The calendar is real (2026-08-05) — /social/calendar renders pillars.json with this ISO week's rotation picks highlighted (a TS mirror of the seeder's math, parity-tested), a queue chip per brief, and Queue now to file one immediately. Calendar → draft is a click.
  • The queue drains itself (2026-07-30) — scripts/drain-drafts.sh --install arms a launchd tick every 5 minutes: heartbeat on every look, Claude only when asks are pending. The window reads the heartbeat back (position, last check), stops polling after 10 minutes, and the drain sweep expires hour-old unanswered asks — a stall now reads as a stall, not an outage.
  • Still pending — adapters beyond Slack (see Hermes), per-brand Telegram channels, a PostHog destination to consume the UTMs, and per-channel drafting in the cron. Tracked in docs/SOCIAL-AUTOMATION.md.

Production Scenarios & Playbooks

The pipeline executes through six canonical, end-to-end production scenarios:

ScenarioObjectiveTools & PipelineDocumentation Link
Mkan Port Sudan TourCoastal rental walkthroughs & listing packsMedia Studio (9:16 Video · 4:5 Stills) → Twenty CRM → Draft Agent → Multi-channel publishMkan Walkthrough Playbook · Mkan Brand Page
Hogwarts Admission CampaignSchool SIS marketing & zero-paperwork pushMedia Studio (16:9 Ad) → 5-Slide Carousel → Thmanyah Quote Card → LinkedIn / Meta publishHogwarts Admission Playbook · Hogwarts Brand Page
Airbnb Ad RepurposingAdapt proven Airbnb campaigns to MkanShowroom Airbnb Shelf → "Use in Studio" → 121 Port Sudan listingsAirbnb Swipe Playbook · Twenty CRM Schema
Deterministic Quote CardsHigh-trust typography without AI lettering hallucinationsTemplate Lane → HTML Canvas Engine → Live Visual Card PreviewDeterministic Cards Playbook · Carousel Docs
High-Impact Video Production24fps walkthroughs with Red Sea audio foleyPrompt Compiler → Google Veo / Seedance / Kling → Showroom Brief QueueVideo Production Playbook · Google Lane
Full Lifecycle BroadcastWeekly calendar rotation to measured reachPillars rotation → Draft Agent → Review Queue → Scheduled Cron Drain → UTM MetricsSocial Architecture · Channels Mix

See also

  • Media Studio & Generation — photo and video generation, prompt compiler, showroom shelves.
  • Status — what is configured and live right now, versus what is merely wired.
  • Ownership & Accounts — who actually holds the Meta assets, and what Instagram needs.
  • Channel Setup — per-platform setup guides, written to be usable on your own accounts.
  • Carousel — multi-slide bilingual decks rendered at exact platform sizes.
  • CRM & Listings Schema — Twenty CRM listings integration.
  • Brand & Voice — the rules every post obeys.
  • Connectors — the MCP-backed service integrations.
  • /higgs skill — media generation · calendar · draft · approve · publish · measure · carousel · growth agent.