Skip to content

keryx publish

Description

Publish an article's approved outlets to a social platform. (Until spec 0061 this command was keryx post; post is now the noun for a social-media post of an article, and publish is the verb.) The target is a platform name (instagram/youtube/tiktok/linkedin), all (every enabled + approved outlet in scope), or due (the approved outlets whose hour has come — the unattended CI entrypoint). The scope is --article <dir> (the whole announcement set), narrowed with --reel <slug> or --post <id> to one piece, or --workspace <slug> (a reel, through its bundle:); publish due with none sweeps every article the project's content.sources name, which is what a scheduled pipeline runs, hourly.

Posting is gated and idempotent:

  • Refused unless approved (keryx approve first) — the accidental-posting guard (R-POST-2).
  • An already-posted platform is a no-op unless --force (R-POST-3); on success it records posted + posted_at + post_url in social.json.
  • --dry-run runs the same pre-flight a real post does and posts nothing (R-POST-1): the platform's media contract (a reel adapter refuses a post with no video, 0060 D3), the rendered reel's presence, and the copy against the platform's constraints. It then reports whether a credential was there to construct the publisher, as credentials: present or credentials: absent, and passes either way, so a pipeline holding no secrets can still prove a set is sendable.
  • post all posts each platform independently — one platform failing doesn't abort the others, and the run exits non-zero if any failed (R-POST-5..8).
  • post due is the only command CI runs unattended (R-POST-13).
  • One outlet per platform per run. A platform with several outlets due (a reel and a post, say) sends the earliest due; the rest report deferred and go on the next run (spec 0060 D5).

A reel piece sends its workspace's reel-<slug>.mp4 (reel build's output); a post piece sends its image (the bundle file, or the public URL for a platform that takes only a URL) when it has one, and text alone when it has not, if the platform's contract allows. The copy comes from the article's social.json (keryx social).

Usage

keryx publish <platform|all|due> [--article <dir> [--reel <slug> | --post <id>] | --workspace <slug>] [--dry-run [--require-credentials]] [--force]

keryx publish due                                          # the hourly pipeline job: every article, every due outlet
keryx publish all --workspace my-post --dry-run            # prove the reel's outlets are sendable, no secrets needed
keryx publish bluesky --article content/post/my-post --post launch   # send one post's outlet now

Platform adapters land incrementally (Instagram → YouTube → TikTok → LinkedIn); a platform with no compiled-in adapter reports clearly. The approval gate, idempotency, scheduling, and fan-out work today against the posting state machine.

With an object store configured (spec 0039), post first pulls any media a reel piece's workspace pins in media.lock but is missing locally — the rendered reel included — so a fresh CI clone posts without an explicit keryx media pull.

Instagram (available)

The Instagram Reels adapter posts to a Professional (Business/Creator) account under Standard Access (no App Review). Configure:

  • platforms.instagram.enabled: true and platforms.instagram.user_id: <id> in config (non-secret).
  • INSTAGRAM_ACCESS_TOKEN in the environment (a long-lived token with instagram_business_content_publish) — never committed; keryx auth instagram will capture it (auth lands next).

It runs the container-publish flow with the resumable direct upload (no public URL): create a REELS container → upload → poll to FINISHED → publish → record the permalink. The caption is the platform's text + hashtags.

Flags

Flag Description Default Required
--article the article's bundle directory (its announcement set)
--reel narrow to one reel piece (a workspace slug)
--post narrow to one post piece (its id)
--workspace shorthand for --article <its bundle> --reel <slug>
--dry-run run the pre-flight, report credential presence, send nothing
--require-credentials with --dry-run, fail when a credential is absent rather than reporting it
--force re-send an already-published outlet