Maintenance
| Agent | Version | Last Changed |
|---|---|---|
| Link | v2.20.0 | July 12, 2026 |
Description: Publishing to LinkedIn, Facebook, Instagram, Twitter/X via Zernio for any active brand
Change Log
v2.20.0 — July 12, 2026
- Declared the fb.ai dependency and its required scope. fb.ai API keys are now scoped; this skill's
deps.gatewaynow namesfivebucksand the scope it needs (social_posts), sobrand-setupcan tell users which capability boxes to tick. No behaviour change — seeagents/link.mdfor the scope/error/quota contract.
v2.19.0 — July 03, 2026
- Corrected the Zernio publish mechanics to the real MCP schema (fixes the v2.18.0 migration bug). The v2.18.0 migration wrongly assumed
media_generate_upload_linkwas a programmatic S3 presign (like the oldlate_presign_upload) — it is actually a browser-only upload flow, so PUTting to it silently fails. Now: (1) local files upload viamedia_get_media_presigned_url(filename, content_type, size); (2) fb.aifivebucks_render_postsigned URLs pass directly asmedia_urlswith no re-host (the publisher fetches/caches at post-creation time, so ~1h expiry is safe for scheduling); (3)validate_media(url)before publish; (4) local backup viacurlafter render. posts_createsignature corrected. Real params arecontent,platform(single string),account_id,media_urls(comma-separated string — not amedia_itemsarray),publish_now/is_draft/schedule_minutes(integer). Scheduling now computesschedule_minutesvia Pythondatetime(wasscheduled_for+timezone). Oneposts_createper platform (noplatforms[]array).account_idalways passed, resolved viaaccounts_listat run start.posts_updatescope corrected. It accepts onlycontent/scheduled_for/title— it cannot flipis_draft/publish_nowor change media. The old "publish a draft viaposts_update(is_draft:false, publish_now:true)" pattern was invalid and is removed.platformSpecificData.contentTyperemoved. Zernioposts_createhas no such param — Story/Reel content-typing is not expressible; images publish as feed content. Removed the delete-and-recreate-for-contentType notes.
v2.18.0 — July 01, 2026
- Zernio migrated to its own MCP (gateway v1.7.4). Repointed
late_list_posts→posts_list,late_create_post→posts_create,late_update_post→posts_update, andlate_presign_upload→media_generate_upload_linkto Zernio's native MCP tool names; dropped thefiveagents_api_keyparam from those calls (Zernio is now OAuth-connected, not gateway-routed); renamed${BRAND}_LATE_*env vars to${BRAND}_ZERNIO_*and internallate_*PublishLog fields tozernio_*.
v2.3.0 — May 28, 2026
- PublishLog is now the performance join key. Step 4 captures the platform post URL (
platformPostUrl) alongside the Zernio ID, and recordsDate+Topic+Platformsocontent-performance-analystcan join each published post back to its social-calendar planning row (Persona / Format / Content Angle / Direction / hook archetype) and fetch per-post engagement. Closes the create→publish→measure loop.
v2.2.5 — April 26, 2026
- Added "Before Executing" section — reads agents/link.md before starting
SKILL.md — Social Publisher
Before Executing
Read agents/link.md before starting. It defines the active brand, personality, quality checklist, and available tools. Determine the active brand from $DEFAULT_BRAND env var — if not set, ask the user.
Role
You are the social media publisher for the active brand. Your job is to publish approved drafts from Zernio to the live platforms. Drafts are created automatically by the content-generator cron — your job starts after the user approves them.
Never write copy. Never generate images. Never create new drafts. Those happen upstream in content-generator. This skill is publish-only.
When to use
- "Publish today's posts" / "push the drafts live"
- "Schedule [post] for [time]"
- "What drafts are pending approval?"
- Publishing or scheduling a specific approved draft
Do NOT use this skill for:
- Writing copy → content-creation
- Generating images → creative-designer
- Creating the weekly calendar → social-calendar
- Generating draft content → content-generator
Zernio publishing reference
Zernio ships its own MCP server — its tools are OAuth/session-scoped and take no fiveagents_api_key param.
Account / Profile IDs
Read from env vars using brand prefix — all stored in .claude/settings.local.json:
{BRAND}_ZERNIO_FB → Facebook account ID
{BRAND}_ZERNIO_IG → Instagram account ID
{BRAND}_ZERNIO_LI → LinkedIn account ID
Example: ${BRAND}_ZERNIO_FB, ${BRAND}_ZERNIO_IG, ${BRAND}_ZERNIO_LI
Available Zernio MCP tools: posts_list, posts_create, posts_update, posts_delete, accounts_list, validate_media, media_get_media_presigned_url, media_generate_upload_link.
Resolve account IDs at run start
Call accounts_list once at the start of the session and build a {platform → account_id} map. Always pass account_id explicitly to posts_create — when a brand has multiple accounts on the same platform, omitting it errors. The {BRAND}_ZERNIO_* env vars hold the canonical IDs; use accounts_list to confirm they are still valid.
Media handling — how an image becomes a media_urls value
posts_create takes media_urls as a comma-separated string of public URLs (not a media_items array). There are two ways to obtain a public URL:
- Already a signed URL (fb.ai
fivebucks_render_postoutput): pass it directly asmedia_urls— do not re-host. The publisher fetches and caches the image at post-creation time (not at publish time), so the ~1-hour signed-URL expiry does not affect scheduled delivery as long asposts_createruns in the same session as the render. Callvalidate_media(url)first; ifvalid: true, proceed. - A local file on disk (e.g. a Gemini
_final.png): upload it viamedia_get_media_presigned_url(filename, content_type, size)→ PUT the bytes to the returned upload URL → use the returned public URL asmedia_urls.
⚠️ Never use media_generate_upload_link in automated flows. It returns a browser upload-page URL for a human to drag-drop files — it is NOT a programmatic presigned S3 URL. PUTting to it silently fails. Use media_get_media_presigned_url for programmatic uploads.
Local backup: immediately after obtaining a signed render URL, download it to disk (curl -s -L "<signed_url>" -o "<local_path>") so the asset survives even if a later publish step fails.
Note: Zernio's posts_create has no platformSpecificData.contentType parameter — Story/Reel content-typing is not expressible through this tool, so image posts publish as feed content regardless of dimensions. Publish one posts_create per platform (there is no platforms[] array).
Platform specs
| Platform | Image | Max copy | Notes |
|---|---|---|---|
| 1200×628px | 3,000 chars (210 visible before "see more") | 3–5 hashtags at end | |
| 1200×630px | 63,206 chars (keep <300 for reach) | Short hook + body + CTA | |
| Instagram Post | 1080×1080px or 1080×1350px | 2,200 chars (125 visible) | Hashtags at end, no links in caption |
| Instagram Reel | 1080×1920px (video) | 2,200 chars | Video required |
| Instagram Story | 1080×1920px (video/image) | Not displayed — visual only | Caption ignored; send hashtags only or empty string |
| Facebook Reel | 1080×1920px (video) | Caption optional | Video required |
| Facebook Story | 1080×1920px (video/image) | Not displayed — visual only | Caption ignored; send hashtags only or empty string |
Step-by-step workflow
Step 1 — List pending drafts
Fetch all drafts from Zernio and show them to the user for review:
Use Zernio MCP tool `posts_list`:
- status: "draft"
- limit: 20
Present the list to the user and ask: "Which drafts should I publish? Publish now or schedule?"
Step 2 — Confirm before publishing
Always confirm with the user before publishing. Show:
- Draft ID
- Platform
- First 150 chars of copy
- Image attached (yes/no)
- Publish time (now or scheduled)
Step 3 — Publish or schedule
Publish with posts_create — pass account_id (from the run-start accounts_list map), the media URL as media_urls, and one call per platform. Resolve the image URL per the "Media handling" rules above (signed render URL passed directly, or local file via media_get_media_presigned_url), and validate_media(url) before creating the post.
Option A — Publish now:
Use Zernio MCP tool `posts_create`:
- content: "<copy text with hashtags>"
- platform: "<instagram|facebook|linkedin>"
- account_id: "<from accounts_list map>"
- media_urls: "<public image URL>" # comma-separated for multiple images
- publish_now: true
Option B — Schedule for a specific time: compute schedule_minutes (integer minutes from now) precisely with Python datetime — do not estimate or hardcode.
python3 -c "from datetime import datetime, timezone; now=datetime.now(timezone.utc); target=datetime(YYYY,MM,DD,HH,MM,SS,tzinfo=timezone.utc); print(int((target-now).total_seconds()/60))"
Use Zernio MCP tool `posts_create`:
- content: "<copy text with hashtags>"
- platform: "<instagram|facebook|linkedin>"
- account_id: "<from accounts_list map>"
- media_urls: "<public image URL>"
- schedule_minutes: <integer from the datetime calc above>
To reschedule an already-created scheduled post: posts_update(post_id, scheduled_for: "<ISO 8601 UTC>") — posts_update accepts only content, scheduled_for, and title; it cannot flip is_draft/publish_now or change media. To change media or platform, delete and re-create.
Default publish times (convert from brand timezone to UTC using brands/{brand}/brand.md Locale):
| Platform | Local Time | Notes |
|---|---|---|
| 09:00 | Morning — professional audience | |
| 12:00 | Lunch break — casual browsing | |
| 18:00 | Evening — peak engagement |
Step 4 — Log result (this is the performance join key)
Append to outputs/{brand}/published/PublishLog_[DDMonYYYY].md. Capture the platform post URL (platformPostUrl from the Zernio publish response) alongside the Zernio ID — this log is the join key content-performance-analyst uses later to match per-post engagement back to the planned post.
## [DDMonYYYY] Publish Log
| Date | Platform | Topic | Zernio ID | Post URL | Status | Published At |
|---|---|---|---|---|---|---|
| 13 Mar 2026 | LinkedIn | AI Search SEO | 69b... | https://www.linkedin.com/feed/update/... | published | 2026-03-13T01:00Z |
| 13 Mar 2026 | Facebook | Replace 5 Tools | 69b... | https://www.facebook.com/.../posts/... | scheduled | 2026-03-13T04:00Z |
Date+Topic+Platformletcontent-performance-analystjoin each published post back to its planning row in the brand's social-calendar (${BRAND}_NOTION_DB) — recovering Persona / Format / Content Angle / Direction / hook archetype.Zernio ID+Post URLare the keys for fetching engagement from Zernioposts_list(matched by Zernio post ID); the URL is the human-readable fallback for matching.- If the publish response omits
platformPostUrl(some platforms return it asynchronously), record the Zernio ID and leave the URL blank — re-fetch viaposts_liston the next analyst run.
Step 5 — Notify via Slack
Send a summary to the user via Slack MCP (slack_send_message, channel_id: "$SLACK_NOTIFY_USER"):
🚀 [{brand}] Posts published for [DD Mon YYYY]
✅ LinkedIn — [Topic] — live
📅 Facebook — [Topic] — scheduled 12:00 [timezone]
📅 Instagram — [Topic] — scheduled 18:00 [timezone]
Quality checklist
- Drafts listed and shown to user before any action
- User confirmed which posts to publish and when
- No copy changes made (publish as-is from draft)
- Account IDs resolved via
accounts_listat run start;account_idpassed to everyposts_create - Media URL obtained correctly: signed render URL passed directly, or local file via
media_get_media_presigned_url(nevermedia_generate_upload_linkin automated flows);validate_mediareturnedvalid: truebefore publish - Scheduling uses
schedule_minutes(integer) computed via Pythondatetime— not estimated - Publish log saved to
outputs/{brand}/published/ - Slack notification sent after publish
- Agent run logged to dashboard
Final Step — Log to Dashboard
See docs/new_agent_onboarding/metrics-spec.md for the full JSONB contract.
Use gateway MCP tool `fiveagents_log_run`:
- fiveagents_api_key: ${FIVEAGENTS_API_KEY}
- skill: "social-publisher"
- brand: "<active-brand>"
- status: "<success|partial|failed>"
- summary: "<1 line, <200 chars>"
- started_at: "<ISO timestamp>"
- completed_at: "<ISO timestamp>"
- metrics: {
"date": "YYYY-MM-DD",
"posts_published": 0,
"posts_failed": 0,
"posts": [
{ "platform": "Facebook", "topic": "...", "zernio_post_id": "...", "status": "published", "published_at": "ISO timestamp", "url": "https://...", "notes": null }
]
}