instagram-publisher pinned
Every 3 hours, post exactly the Instagram carousels the owner approved: Context issue `done`, `due` ≤ now < `due` + 3 h, no live link yet — upload the slides to Postiz with `POSTIZ_API_KEY` (by name, host only), schedule the post on the tenant's Instagram channel found by name, `publish` the instagram_post on the Blog MCP, and post the Postiz id, then the permalink, on the issue. Dedupe guard before any call; idempotent; refuses anything that is not `done`; degrades to owner self-publish without a key.
Install
curl -fsSL https://agents.onecontext.me/skills/instagram-publisher@v10.md -o .claude/skills/instagram-publisher/SKILL.mdDepends on
Files
scripts/postiz.sh1.9 KB
SKILL.md
Instagram publisher — approved → scheduled → live
You run inside the tenant's 3-hourly publisher routine (after the blog
pass), one tenant per run, and you are the only skill that talks to Postiz.
Approval truth is Context: an issue is postable only when its state is
done — the owner approved it through the checker's request_review — and
its slot has arrived. You make no content decisions; you verify, post,
record, report.
Inputs — fetch, never ask
- Tenant — slug, epic id, timezone from the routine prompt (or
CONTEXT_BLOG_TENANT). State the tenant before any write. - Parent — the epic's
Instagramparent (list_issues {parent_id: <epic>, label: "lane:instagram"}→<instagram parent>; the routine prompt or the epic's## Structuremay carry the id — confirm withget_issue). Every Instagram issue is its child (rules-blog§3). - Session start — Context
start_context; Blog MCPusage_guide+get_capabilities:publish,instagram_post_listintools[](elserules-blog§8). - Key —
POSTIZ_API_KEYby name, read insidescripts/postiz.sh(env or the host's secret store).postiz.sh integrationsexits 3 without it: postPublish skipped: POSTIZ_API_KEY not available on this host — owner self-publisheson the issue once and stop. Never echo, log or store the value. - Channel —
postiz.sh integrations→ the entry withidentifier: "instagram"(notdisabled) whosenamematches the tenant's Instagram account in the design tokens / epic## Rules(Meetly: the one named "Meetly …"). One match → itsidfor this run. Zero or several → post why on the issue and stop. Never hardcode an integration id in a skill, prompt or repo. - Now — the host clock in the tenant timezone. Window
[due, due + 3 h).
1. Candidates
list_issues {parent_id: <instagram parent>, state: "done", includeClosed: true, label: "channel:instagram"} — follow nextCursor — then get_issue
each (the list is a hint; the record is the truth). Sort by dueAt:
| condition | action |
|---|---|
activity text starts with Published: |
live — skip |
activity text starts with Scheduled: and none with Published: |
scheduled earlier — §4 (permalink pass) |
now < due |
early — leave it |
due ≤ now < due + 3 h |
post now (§2) |
now ≥ due + 3 h, nothing scheduled |
missed window (§5) |
Blog and site issues belong to blog-publisher; this skill touches
channel:instagram only.
2. Post one issue
Pre-flight — every line must hold, otherwise skip and post why on the
issue once (post_comment, never repeated on later runs):
get_issuenow showsstate.category: "done". Notin_review, notin_progress, never the list's word for it.- The latest
docKind: "review"document is a pass verdict and the slide previews (docKind: "preview") or the asset ids are on the issue. - The post id: the
instagram-post: <id>line the drafter posted; if absent,instagram_post_list {tenant_slug}and matchcontext_issue_id. None → the drafter never finished; skip. - Dedupe guard — all three, before any Postiz write:
- no
Scheduled:/Published:update on the issue (re-read now); instagram_post_listshows the post notpublished(apublishedrow means apublish_eventsentry exists → postPublished:from it and continue with the next issue);postiz.sh posts <due − 1 h> <due + 1 h>has no post on this integration whose content starts with the caption's first line. If it has one, record it asScheduled: postiz <id> …and do not create another.
- no
Post:
- Slides, in order: the assets of the post (
instagram_post_list→slides[]→public_url, or the attached previews).curl -sSL -oeach to a temp dir,postiz.sh upload <file>→{id, path}; keep the order. - Body file (never inline the key; the script adds it):
{"type": "schedule", "date": "<due, ISO 8601 UTC>", "shortLink": false, "tags": [], "posts": [{"integration": {"id": "<integration id>"}, "value": [{"content": "<caption>", "image": [{"id": "<id>", "path": "<path>"}, …]}], "settings": {"__type": "instagram", "post_type": "post"}}]}date=due(already inside the window → Postiz posts at once).postiz.sh schedule <body.json>→ the post id(s). - Blog MCP:
publish {tenant_slug, instagram_post_id, assert_context_done: true, context_issue_id}→{url, published_at, action, publish_event_id}. That assertion means "the Context issue isdone"; you verified it in step 1 of this run, never earlier. If the server refuses because the row is notapprovedand noinstagram_post_set_statustool exists on this contract, postPublish record pending: <code> — <message>on the issue and still keep the Postiz schedule (the owner approved; the audit row waits for the server slice); never retry more than once per run. - Record once:
post_comment {parent_id: id, body: "Scheduled: postiz <post id> at <date> · instagram_post <id> · publish_event <id or pending>", workStats}(role: "publisher").
Errors: postiz.sh non-zero → post_comment {parent_id: id, body: "Publish failed: postiz <command> — <http status / message>"}, no retry this run,
next issue. Never leave a half-scheduled post silent: if schedule failed
after uploads, say so; uploads are harmless. The daily brief reports it.
3. Owner adds music
Instagram cannot attach music to an image carousel through the API. When
the issue asks for music, the owner posts from the Instagram app instead:
post Owner posts manually (music requested): slides and caption attached above once and treat the issue as scheduled; §4 still records the
permalink when the owner pastes it.
4. Permalink pass
For issues with Scheduled: and no Published:: postiz.sh posts <due − 1 h> <due + 1 h> → the post with that id. State PUBLISHED with a
releaseURL → post_comment {parent_id: id, body: "Published: <releaseURL> at <publishDate> · postiz <id>"}. State ERROR → Publish failed: postiz reported <error> once; the owner decides. Anything else → wait for the
next run. A comment from the owner containing an instagram.com/p/ URL
counts as the permalink.
5. Missed windows
now ≥ due + 3 h, nothing scheduled, no Missed window comment: next free
Instagram slot from the epic's ## Rules / decision record (Meetly: Mon
09:00 · Tue 07:00 · Thu 06:00 · Thu 21:00 Asia/Singapore) where no other
channel:instagram child of <instagram parent> is due →
update_issues {ids: [id], due: <next slot>} + post_comment {parent_id: id, body: "Missed window <old due>: re-slotted to <new due> — <reason if known>"}. Never post late silently. An issue already carrying a Missed window comment with the current due is left alone.
6. Idempotency
Read before every write; dedupe on the Scheduled: / Published:
comments, on instagram_post_list status and on the Postiz window
listing. Each issue is touched at most once per run; one run at a time (the
routine wrapper holds a lock). Nothing here edits a caption, a slide, a
verdict or due except the re-slot in §5. Deleting a Postiz post
(postiz.sh delete) happens only when the owner asks on the issue.
7. Report and stop
Print one line for the routine log, which the daily brief reads:
instagram-publisher: <ISO> · scheduled <n> [<tickets>] · live <n> · skipped <n> (<reasons>) · missed <n> · <ok | stopped: reason>. Stopped early → a
handoff comment on the affected issue (rules §7).
Never
Post an issue whose state is not done · post before due · touch blog or
site items · create issues · approve anything · edit a caption or slide ·
create a second Postiz post for the same issue · hardcode an integration
id · retry a failed schedule more than once per run · store, echo or log a
key value (POSTIZ_API_KEY, BLOG_ACCESS_KEY by name only).
Changelog
- v3 v10
- feat(skills): v3 tool-name migration + new context skill (CONT-507)
- v2 v4
- feat(skills): epic structure v2 — channel parents, one Backlog, existing-epic setup + routines step, epic Runs block; manifest v4 (CONT-454, E12)
- docs(E9): mini-spec, acceptance tests, release for CONT-437; attach-artifact.sh; Postiz field fix
- feat(skills): instagram-drafter + instagram-publisher v1, blog-agent v3, manifest v3 (CONT-437, E9)