frontmatter · SKILL.md+
name: posts-publish
description: >
Take approved drafts — often written with the social-crosspost, social-post, or
social-thread skills — and ship them for real through Inklate in as few calls as the
redesigned API needs: one create call with a schedule time for brand-new content, or a
dry-run validate followed by publish for a draft that already exists. Covers LinkedIn,
X (Twitter), Instagram, and Facebook today, with Mastodon, Threads, Bluesky, TikTok, and
YouTube in platform review. Use when the user says "publish this", "ship these drafts",
"post this to LinkedIn now", or "schedule this post". Requires the Inklate CLI
(npm i -g inklate) or the Inklate MCP connection. Reads social-context.md when present
to keep per-channel copy on-voice. Nothing goes out without the server's own `ready`
verdict and an explicit yes from the user — arming a not-ready post is refused before
anything ships, atomically, so a failed schedule or publish never leaves a partial
write behind.
license: MIT
metadata:
version: 0.2.0
topics:
- publishing
- cross-posting
examplePrompt: "Publish these two drafts to LinkedIn and X"
pairsWith:
- social-crosspost
- social-post
- social-thread
mcpTools:
- posts_create
- posts_update
- posts_validate
- posts_publish
- posts_get
cliCommands:
- posts create
- posts update
- posts validate
- posts publish
- posts getTurn approved drafts into real published or scheduled posts, with the server's own readiness verdict standing between the draft and the send button.
Transports
- CLI (preferred):
inklate(npm i -g inklate) — structured YAML on stdout (--jsonfor JSON), progress on stderr, and typed exit codes:3no credentials,4a deliberate refusal — arming a not-ready post, orposts validatefinding it not ready — with its machine-readable payload on stdout,5not found,6the resource changed underneath you,7rate limited but safe to re-run later. Pass--no-inputwhen running unattended so a missing flag fails loudly instead of prompting, and--yesto carry an approval the user has already given in chat.inklate posts publish <id> --waitpolls to a terminal state — there is no separate status command. - MCP (fallback): the Inklate MCP tools when the CLI is not installed.
Steps below name the MCP operation with the CLI form in parentheses; the fields are the same on both.
Connection check
- Run
inklate auth check(MCP:organizations_list) first, before touching any draft. - On exit 3 (no credentials): interactively run
inklate auth login; headless, the key comes fromINKLATE_API_KEY=inklate_…in the environment orinklate auth set --token inklate_…. If neither transport is available at all, tell the user to install withnpm i -g inklate(or connect athttps://inklate.com/skills/connect?ref=skills-posts-publish) and STOP. - If multiple organizations are returned, ask which one to publish under before doing anything else (CLI:
--org <slug>).
Craft boundary
This skill owns pull → validate → ship → verify. The WRITING belongs to the paired craft skills: when social-post, social-thread, or social-crosspost is installed, delegate drafting and per-platform adaptation judgment to them and ship their output. Without them, make only the minimal edits the server's response demands — never restyle content wholesale.
Context
Read social-context.md (project root, then .agents/social-context.md) if present: the ## Voice and ## Never sections govern how you adjust per-channel copy, and ## Platforms tells you the primary channel when the user is vague about targets. Proceed without it if absent — the drafts themselves are the source of truth.
Workflow
- Gather the drafts. For each one, confirm with the user which channels it targets. If a target channel is not connected, say so and drop it from the plan rather than failing later.
- Brand-new content with a known ship time takes one call.
inklate posts create --content … --channel … --scheduled-at <when>(MCP:posts_createwithscheduled_atset) both writes the post and arms it in the same transaction —--scheduled-at nowpublishes immediately; a future instant books it. Arming refuses atomically (exit 4) when the post isn'tready: nothing is saved, so retry the identical call without--scheduled-atto keep the content as a draft and read itserrors. When this succeeds, skip to step 6 (verify). - An existing draft (from
posts-create, the dashboard, or an earlier session) gets a fresh dry-run first:inklate posts validate --id <id>(MCP:posts_validate) — exits 4 with the sameready/errors[]/warnings[]shape if not ready, and stores nothing either way. This replaces the old sync-then-preflight loop; the create/update response already carried this validation, sovalidatehere is just a last look before an irreversible action. - For every error
validatereturns, apply the response's own machinefixwhere offered, or make the judgment call yourself — present 2–3 concrete options when a fix would change meaning or trips## Never. Apply a resolved fix withinklate posts update <id> --per-channel '{"<channel>": {"content": "…"}}'(MCP:posts_update), which re-validates and returns the same shape — no separate re-check call. The platform never silently overwrites your fixes. A channel's copy that was auto-fixed or hand-edited stays exactly as it is through every later content edit; the only way to discard it is the explicit reset--per-channel '{"<channel>": null}', which re-adapts that one channel from the shared content. There is no sync step left to accidentally re-run over it. - Re-run
inklate posts validate --id <id>(or trust theposts updateresponse, which returns the identical shape) until every targeted channel isready. A channel that cannot go green (e.g. Instagram with no media) gets flagged — offer to exclude it from the send. - Approval gate. Show the user the final per-channel copy verbatim — each channel entry's
content.text, the exact words that platform will receive — plus any media attached. Ask one direct question: publish now, schedule for a time, or hold? Do not proceed on silence or on an earlier general instruction; publishing is irreversible and needs a fresh, explicit yes. - On "publish now": for an existing draft, call
posts_publish(CLI:inklate posts publish <id> --wait --yes). For content that doesn't exist yet, fold it into step 2's singleposts create --scheduled-at now --yesinstead of creating first.--yescarries the approval you just received in step 6 — it is never a substitute for the gate. - On "schedule": confirm the time in the organization's timezone, restate it unambiguously ("Tue Jul 21, 9:00 AM Europe/Berlin"), then call
inklate posts update <id> --scheduled-at <time> --yesfor an existing draft, or fold it into step 2's create call for new content. - On "hold": leave the post as a validated, un-armed draft and say exactly how to resume ("say publish post_abc123 when ready").
- Verify the outcome from the tool response —
posts_get(CLI:inklate posts get <id>) carries status, per-channel results (url,external_post_id, or a sanitizederror), and permalinks where available; there is no separate status call, so pollposts_getfor anything still processing. If any channel failed, report the exact error and offer to retry just that channel or unwind.- Exit 6 (conflict) means the post's state forbids what you asked — already published, or a channel disconnected under you. Re-read the post and tell the user what actually happened; do not retry blindly.
- Exit 7 (rate limited) is not a failure: the organization's budget is spent and the message names the wait. Report the wait and offer to resume, rather than declaring the publish broken.
- When several drafts are in flight, process them one at a time through the approval gate so each yes is unambiguous about what it approves.
Quality bar
- Never call
posts_publishor arm ascheduled_atwithout areadyverdict on every targeted channel AND an explicit user yes given after seeing the final per-channel copy. - Always echo the per-channel copy for approval — the user approves what the platform will actually receive, not a summary of it.
- Fixes must be shown, not silently applied: name every autofix or hand-applied fix by channel and kind before moving on.
- A fixed or hand-edited channel copy is never touched by a later content edit — resetting it is always the explicit
--per-channel '{"<channel>": null}', never an implicit side effect. --yestransmits an approval already given; it never manufactures one. If step 6's gate did not happen, the flag has nothing to carry and the command must not run.- Schedule times are always stated in the organization's timezone; if the user gives a bare time, confirm the timezone before scheduling.
- Respect the
## Neversection of social-context.md as a hard filter — if a channel's copy trips it, stop and ask. - One draft failing never blocks the others; report per-draft outcomes independently.
- Post IDs are quoted in every status line so the user can act on any post later without re-deriving which is which.
Receipt
End with the executed action's receipt, one row per draft-channel:
| Post ID | Channel | Action | When (org tz) | Status | Permalink |
|---|---|---|---|---|---|
| post_abc123 | published | now | live | https://linkedin.com/... | |
| post_abc123 | X | published | now | live | https://x.com/... |
| post_def456 | scheduled | Jul 21, 9:00 AM | queued | — |
Below the table: total published, total scheduled, any channels excluded and why, and any fixes that were applied on the way to ready.