Publishing posts
Compose, schedule, and approve a post so Oxygen publishes it on time — and nothing goes out that a human did not approve.
Publishing holds scheduled posts, gates them behind a human approval, dispatches them to LinkedIn, X, Instagram, TikTok, Facebook, and YouTube, and records every attempt. Performance and public comments on what shipped belong to Posts.
Only an approved post publishes. A publish time is not permission — an unapproved post sits in the queue past its scheduled minute. Every content edit revokes approval, because approval binds the exact copy reviewed: change a word, approve again.
/publishing route sits behind the OXYGEN_PUBLISHING_ENABLED switch. When it is off the routes 404 and the Publishing sidebar item is gone — there is no setting you can flip yourself.Compose
In the sidebar: Publishing (/publishing) → New post (/publishing/posts/new).
Click a chip in the channel row to add that channel as a target and focus its composer; each target keeps its own body and its own limit (LinkedIn 3,000 characters, X 280 — or 25,000 when the selected X account is Premium — Instagram 2,200, YouTube Shorts 100). TikTok is not offered in the web channel row today; the CLI and MCP still accept it, and existing TikTok posts keep their stored configuration. Add a first comment posts a comment automatically right after the post — every composer channel except TikTok. The right rail holds the schedule picker, a comma-separated Tags field, and Schedule / Post Now.
Both buttons approve as they save: you just read the post. On an existing post the composer flushes the pending autosave first, so approval binds what is on your screen.
The CLI creates a post unapproved unless you pass --approved:
oxygen publishing posts create --publish-at 2026-09-01T09:00:00Z \
--sender <sender-account-id> --text "Copy. Mention @<public-identifier>." --jsonTag a person with @<public-identifier> from their profile URL (linkedin.com/in/<public-identifier>), and a company page with @<handle> from its page URL (linkedin.com/company/<handle>). Approval looks each tag up and shows who it resolved to; OXYGEN sends a company tag to LinkedIn by the page's numeric id, so it appears as a clickable company tag. A company tag OXYGEN cannot find blocks approval with linkedin_company_mention: fix the handle, or write the name without the @.
--sender is the LinkedIn sender; other channels take --provider and --provider-connection. Facebook has no composer chip — it is CLI-only. Upload media with oxygen publishing media upload <file> and attach the returned id through content.media_asset_ids. A LinkedIn video can carry a cover image: oxygen publishing posts update <post-id> --video-thumbnail-file cover.jpg (a JPEG or PNG; --video-thumbnail <media-id> for one already uploaded, --clear-video-thumbnail to remove it). In the editor, Cover on the video picks a frame or an image, and Preview shows the post as LinkedIn will, video included. Retag with oxygen publishing tags set <post-id> --add launch.
For a creator program, first read oxygen ugc memberships list --json, then choose Personal or one Program before the first save. Program members find this dropdown in the editor breadcrumb; creators without an active program see no ownership selector. Adding --ugc-participation-id <id> to publishing posts create saves the personal post and enrolls it in that program as an unapproved draft. It refuses --approved and scheduled status; enrollment itself never authorizes publication.
Before publication starts, the creator can run oxygen ugc posts remove with the active enrollment_id and current source_revision. The program enrollment is removed while the post remains a personal draft. A later enrollment has a new enrollment_version and needs fresh creator and brand approvals. UGC edits, approvals and retained-history reads should always use the version returned by oxygen ugc posts list.
Leaving the whole program is separate from removing one post. In UGC Settings, choose Leave entire program, or use oxygen ugc creators update --participation-id <id> --status revoked --json. Future sharing, program publishing, metric refresh and engagement stop. Your posts, private history and voice stay in your workspace; the organizer retains previously shared snapshots and receipts. You lose access to that program's shared Knowledge. Paid credit lots remain until their period ends; check any pending sponsorship-cancellation notice. If the program paid your LinkedIn account's monthly credit reservation, your own balance carries it from its next renewal unless another program sponsors you; disconnect the account if you no longer need it. Leaving does not delete social posts or shared history. This workspace cannot currently restore the participation or rejoin the same program with another invitation.
X posts
An X post can be a single post or a thread, and the first post can carry a poll or media, never both. Each post takes up to 4 images, or one GIF, or one video, never mixed, and 280 characters (25,000 for an X Premium account). In the editor, add posts to the thread with +, open Poll, pick Who can reply, and use Alt on an image for its description. On the CLI:
oxygen publishing media upload chart.png --json # prints the media id
oxygen publishing posts create --provider x --provider-connection <connection-id> \
--publish-at 2026-10-06T09:00:00Z --draft --text "Post one" \
--content-json '{"media_asset_ids":["<media-id>"]}' \
--thread-post "Post two" --thread-post "Post three" \
--alt-text <media-id>="What the chart shows" --photo-tag XDevelopers \
--reply-settings mentioned --json
oxygen publishing posts create --provider x --provider-connection <connection-id> \
--publish-at 2026-10-06T09:00:00Z --draft --text "Which one?" \
--poll-option Data --poll-option Copy --poll-duration 2d --jsonpublishing posts update takes the same flags and merges them into the post; --content-merge-json '{"poll":null}' removes one. oxygen publishing posts get <post-id> returns publish_plan: every X post that will be sent, in order, with its media, alt text, poll and who can reply, plus the lint findings. It is free and makes no call to X.
Mentions are plain @username text, which X links itself. Photo tags need images on the first post. X's API no longer lets self-serve apps create quote posts, and only accepts a reply to someone else's post when that person mentioned or quoted you, so OXYGEN warns on both.
Approve
oxygen publishing posts list --approval-status needs_approval --json
oxygen publishing posts approve <post-id> --jsonApprove runs a deterministic channel lint, recomputed on every read and never stored. Five findings are errors and block approval: an empty body, a LinkedIn body over 3,000 characters, an Instagram caption over 2,200, a PDF on a channel that cannot carry one, and a poll outside 2–4 options. Everything else — including an over-limit X body, whose tier can only be read from a cache — is a warning that publishes anyway. On LinkedIn, approval also resolves every inline @<public-identifier> and blocks with inspectable details if one cannot be verified. In the web queue this is Approve… in a row's ⋯ menu.
A workflow can create and schedule posts but never approve one: create forces needs_approval and strips any approval field a recipe passes. Cron cannot publish for you.
The queue
/publishing lists every post newest first, the ones waiting on you hoisted to the top and badged Waiting on approval. oxygen publishing posts list returns the oldest first; add --order newest for the latest first. The icon strip beside New post switches between List, Board, and Calendar; tag chips filter the list. A row's ⋯ menu carries Edit post, Approve…, Retry now, Reopen, Cancel…, and Delete…, whichever the state allows. /publishing/posts/<id> shows the attempt history, with Retry on a failed post and Cancel on anything pending.
| Situation | Do this |
|---|---|
| Publishing failed | Inspect the post and its attempt history first; see the recovery guidance below |
| Stop it before it sends | oxygen publishing posts cancel <post-id> |
| Undo that cancel | oxygen publishing posts uncancel <post-id> — reopens unapproved |
| Roll the text back | oxygen publishing posts revisions <post-id>, then restore --revision <n> |
| Remove it entirely | oxygen publishing posts delete <post-id> — refused once attempted; cancel instead, so the attempt ledger stays honest |
Each sender is paced at 25 published posts per rolling 24 hours — a rolling window, not a calendar day. A post over that ceiling is deferred rather than failed, but deferral is not unbounded: once it has been dragged more than 24 hours past its original publish_at, it fails loudly instead of publishing day-old content later. oxygen publishing posts note <post-id> --text "..." leaves an internal note, never published.
Investigate a failed or slow post
Start with oxygen publishing posts list --json. Without a status filter, this includes every status in the returned page; follow its pagination before concluding that a post is missing. Read the exact post with oxygen publishing posts get <post-id> --json for its current error and attempt history. These reads do not publish anything. An unapproved draft has not attempted publication. A published entry imported from LinkedIn is not evidence that Oxygen successfully sent it; inspect the publication attempt receipts.
linkedin_content_too_large means the upload exceeded an accepted size limit. Reduce attachment size or count, or shorten the text, before publishing again. Older records can retain linkedin_write_refused while their displayed explanation correctly identifies a size failure.
A generic linkedin_write_refused does not establish a duplicate, blocked link, or account limit. Check the LinkedIn profile before retrying: if the post already exists, do not publish it again. If it is absent and the refusal persists, contact support with the post link. After resolving the cause and reviewing the exact content, use oxygen publishing posts retry <post-id> only when you intend to publish it.
Post Now requests immediate dispatch; it does not mean LinkedIn has already accepted the post. Queue waiting and the provider upload are separate parts of the delay. Provider cooldowns can defer an approved post, while an interrupted attempt can have an uncertain outcome.
A post locked after an uncertain outcome
publishing_effect_outcome_uncertain means the channel never confirmed whether the post went out, so OXYGEN locks it: retry, edit, and reschedule are refused rather than risk publishing it twice. Only you can settle it. Open the profile and look for the post:
- It is there: nothing is left to publish; leave the post as it is.
- It is not there: choose It wasn't posted on the post page, or confirm on the command itself. The post unlocks, still failed, and shows the channel's own error — fix that cause, then publish again.
oxygen publishing posts update <post-id> --text "Fixed copy" --confirm-not-published --json
oxygen publishing posts retry <post-id> --json # then approve if the edit revoked approval--confirm-not-published (MCP and API: confirm_not_published: true) also works on posts retry when the text needs no change. An X thread whose first posts are already live cannot be confirmed away.
Drafts, ideas, import
/publishing/drafts holds the backlog upstream of the queue — AI variants and parked ideas — each with Accept, Edit, and Reject. Accepting creates a post that still needs approval. Generating copy is deliberately not a button there, because it spends credits.
--max-credits is the ceiling the call cannot cross.oxygen publishing posts draft --template feature_launch --topic "..." --max-credits 20 --json
oxygen publishing posts review <post-id> --max-credits 10 --jsonreview is an advisory voice-and-claims check that never blocks approval. oxygen publishing ideas add --text "..." parks a free idea; oxygen publishing import --file posts.csv is dry-run by default, and --approved writes the rows as needs-approval drafts.
Amplification
Amplification policies have teammates' connected LinkedIn accounts react to and comment on posts in scope, automatically. The web app has no amplification controls today; create, arm, pause and inspect them with the CLI or MCP.
oxygen publishing amplification create --name "Launch week" --scope tag --tag launch \
--senders <id1>,<id2> --actions reaction --max-credits 50 --approved --acknowledge-risk --json
oxygen publishing amplification enable <policy-id> --approved --acknowledge-risk --jsonLinkedIn may warn, restrict or close accounts used for automated engagement. Creating, enabling, posts boost and saving an enabled UGC policy all require that acknowledgement (--acknowledge-risk, or risk_acknowledged: true in the API and MCP); without it the call is refused with amplification_risk_acknowledgement_required and nothing is written. OXYGEN records who acknowledged it, when and from which surface. Policies enabled before this requirement keep running and show risk_acknowledgement: "missing" until they are next edited with --acknowledge-risk or re-enabled.
Pacing limits apply to every policy, whatever it stores:
- At most 12 actions per post. A higher
--max-actions-per-postis refused. - About 60% of the named teammates engage on a given post (always at least one). About a third of those comment; the rest react.
- Reactions land 20 minutes to 6 hours after the post goes live, comments 45 minutes to 24 hours, and no two actions on a post are less than 4 minutes apart.
- A pool comment is not reused within 30 days or twice on one post; when the pool runs out, the teammate reacts instead. An AI comment too close to a recent one is redrafted once, then skipped.
The action ledger (amplification actions) shows each decision, including not_selected, comment_pool_exhausted and duplicate_comment skips. A policy is created disabled; both commands above spend credits and write publicly. disable is never gated and skips every action still planned.
UGC has two independent amplification grants. A program operator's host policy uses the host's accounts, voice pages and wallet. The program's peer-engagement switch only permits creators to opt in: each creator separately chooses their own LinkedIn sender, actions, caps and voice page, and their own wallet pays. Joining a program, accepting sponsorship or delegating post approval does not grant engagement consent. Both lanes start disabled, and an unknown provider effect is reconciled only from observed evidence without retrying it.
UGC amplification excludes the post author. To react or comment on your own post, use the separate oxygen engagement react, oxygen engagement comment or oxygen publishing comments commands after reviewing their help, preview, approval requirements and any cost.
No publishing tool is in the default MCP set — connect with ?toolset=publishing.
Related
- Post analytics and Community — how a published post performed, and its public comments.
- Approvals — the external-write gate everywhere else.