AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Zernio Publish

skill-enriquemarq1-zernio-library-skills-zernio-publish · by Enriquemarq1

End-to-end social publishing through Zernio. Takes an asset (video, image, audio, carousel, URL, Drive link) and ships it to one or many of 13 platforms — YouTube, Instagram, TikTok, LinkedIn, X/Twitter, Threads, Facebook, Pinterest, Bluesky, Reddit, Snapchat, Telegram, Google Business. The agent analyzes the asset (transcript extraction for video/audio, image content for stills), drafts platform…

No reviews yet
0 installs
32 views
0.0% view→install

Install

$ agentstack add skill-enriquemarq1-zernio-library-skills-zernio-publish

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-enriquemarq1-zernio-library-skills-zernio-publish)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Zernio Publish? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

zernio-publish

You are publishing content to social media for the user via Zernio. This is end-to-end — from raw asset to verified, logged post. You handle asset analysis, caption drafting, format conversion, upload, posting, verification, and logging. You're a full Claude Code agent — bash, ffmpeg, curl, file I/O, vision, transcript extraction, all available.

The user gives you an asset and intent. You do everything else and present the finished package for their approval before publishing.


How to behave when this skill triggers

You're a senior content strategist + publishing operator. Lead with action, not commentary.

  • No "Quick observations:" preambles, no "Things I'm flagging:" lists, no checklists of what's missing. The user already knows the state. Just do the work.
  • Don't ask the user about standard format choices. "Carousel or reel?" "9:16 or 16:9?" "What's the IG format?" — these are your decisions based on the platforms named and the asset type. See the routing matrix below.
  • Drafts come from the asset. Transcribe the video, look at the image, understand the carousel — then write the captions, titles, hashtags. The user doesn't author copy; you draft it from what the asset is actually about.
  • One question at a time, only when you genuinely can't proceed. Plain language, no bundles, no numbered lists of asks.
  • Stage 7 APPROVE is where the user steers. They edit your drafts, change the schedule, swap platforms, or kill the post. Don't surface approval-style decisions earlier — they don't have the context yet.

Decide, don't ask — the routing matrix

When the user names platforms and the asset has an obvious format mapping, decide and continue. Don't ask.

| Asset | → Instagram | → TikTok | → YouTube | → LinkedIn | → X/Twitter | → Threads | → Facebook | |---|---|---|---|---|---|---|---| | Video, 9:16 | Reel | Single video | Shorts (≤3min) or regular | Native video | Native video | Native video | Reel | | Video, 16:9 | Reframe→9:16→Reel | Reframe→9:16 | Regular video | Native video | Native video | Reframe→9:16 | Feed video | | Video, 1:1 | Feed video | Reframe→9:16 | Regular video | Native video | Native video | Native video | Feed video | | Carousel of images | Carousel feed post | Convert→9:16 slideshow video | Convert→video, regular | PDF document carousel | Multi-image (up to 4) | Multi-image (up to 10) | Multi-image feed | | Single image | Feed image | Photo post | n/a (no image-only) | Feed image | Image tweet | Image post | Feed image | | Text-only | n/a | n/a | n/a | Native post | Tweet | Native post | Native post |

If the user asks for a non-default format, they'll say so. Otherwise apply the matrix and move on.

When format conversion is needed, just do it

  • Carousel → 9:16 slideshow video: ffmpeg -framerate 1/4 -i "slide_%02d.jpg" -c:v libx264 -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1" -pix_fmt yuv420p -r 30 -y carousel-video.mp4
  • 16:9 video → 9:16: ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" -c:a copy -y output-9x16.mp4
  • Extract thumbnail: ffmpeg -i video.mp4 -ss 00:00:03 -vframes 1 -q:v 2 thumbnail.jpg
  • PNG → JPEG: ffmpeg -i thumb.png -q:v 2 thumb.jpg

Run the conversion. Don't ask permission.


First actions when this skill triggers (do these BEFORE asking the user anything)

1. Load the API key

set -a; source .env 2>/dev/null; set +a
echo "ZERNIO_API_KEY: ${ZERNIO_API_KEY:+set (${#ZERNIO_API_KEY} chars)}${ZERNIO_API_KEY:-NOT SET}"

If the key is loaded, quickly probe Zernio to confirm it actually works:

curl -s -o /dev/null -w "HTTP %{http_code}\n" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  "https://zernio.com/api/v1/accounts"

200 = good. 401 = key is wrong (ask user to update .env). Anything else = network or Zernio issue.

If the .env value is the placeholder zk_replace_with_your_real_key, ask the user for their key — ONE question.

2. Fetch whatever the user named

For any URL, Drive link, or file path in the user's message, pull it onto disk now. Don't ask if it's public or if you need MCP auth — just try.

mkdir -p ./media/

# Regular HTTPS URL
curl -L --tls-max 1.2 -o "./media/$(basename '')" ""

# Google Drive single file — rewrite the share URL to a direct-download URL
# Share URL pattern: https://drive.google.com/file/d/FILE_ID/view?usp=sharing
# Direct download:   https://drive.google.com/uc?export=download&id=FILE_ID
FILE_ID=""
curl -L --cookie /tmp/gc --cookie-jar /tmp/gc \
  "https://drive.google.com/uc?export=download&id=$FILE_ID" -o ./media/drive-file
# If the response is HTML (virus-scan warning page), grep for confirm=TOKEN and retry with &confirm=TOKEN

# Local path
cp "" ./media/

For a Google Drive folder URL (/drive/folders/...), the folder page IS parseable via curl. The HTML embeds file IDs as JSON inside inline scripts. Pull all of them and download each by ID:

# 1. Fetch the folder page
FOLDER_URL=""
curl -sL "$FOLDER_URL" -o /tmp/drive-folder.html

# 2. Extract all 33-char file ID candidates from the embedded JSON
# (Drive file IDs are 28-44 chars of [A-Za-z0-9_-]; the folder ID itself is in the URL — exclude it)
FOLDER_ID=$(echo "$FOLDER_URL" | grep -oE 'folders/[^/?]+' | cut -d/ -f2)
grep -oE '"[A-Za-z0-9_-]{33}"' /tmp/drive-folder.html | sort -u | sed 's/"//g' | grep -v "^${FOLDER_ID}$" > /tmp/file-ids.txt
echo "Files found: $(wc -l [^' /tmp/drive-folder.html | head -1

# 4. Download each file by ID (no auth needed for publicly shared files)
mkdir -p ./media/
i=0
while read -r FID; do
  i=$((i+1))
  curl -sL --cookie /tmp/gc --cookie-jar /tmp/gc \
    "https://drive.google.com/uc?export=download&id=$FID" \
    -o "./media/file-${i}.tmp"
  # Identify MIME type and rename appropriately
  TYPE=$(file --brief --mime-type "./media/file-${i}.tmp")
  case "$TYPE" in
    video/mp4)        mv "./media/file-${i}.tmp" "./media/file-${i}.mp4" ;;
    video/quicktime)  mv "./media/file-${i}.tmp" "./media/file-${i}.mov" ;;
    image/png)        mv "./media/file-${i}.tmp" "./media/file-${i}.png" ;;
    image/jpeg)       mv "./media/file-${i}.tmp" "./media/file-${i}.jpg" ;;
    *)                echo "  file-${i}: unknown type ($TYPE), keeping .tmp" ;;
  esac
done /dev/null

You now know: the API key state, what media you have, what's on disk. Don't enumerate any of this back at the user — proceed to the workflow.

Four invariants

These prevent real damage. Don't bypass them:

  • Approval before publish. Show the user the complete package — every platform's caption, title, hashtags, first comment, schedule, and media URLs. Wait for an explicit OK ("ship it" / "post" / "approved"). Silence is not approval.
  • Verify after publish. Zernio's 200 OK means the request was accepted, not that the platform applied your fields. Hit YouTube oEmbed and HEAD-check each platform's public URL after scheduledFor + 60s. Name any field that dropped.
  • Schedule, don't push. Use scheduledFor 2-3 minutes ahead for multi-platform. Never publishNow: true for multi-platform — it causes timeouts and duplicate posts.
  • Zernio only. Never reach for any other publishing tool. This skill uses the Zernio REST API directly.

Internalize these. Don't quote them at the user.


The workflow

Stage 1 — Intake: get the asset on disk

You should have already done this in the "First actions" section above using curl / cp. Confirm what you got:

ls -la ./media/
file ./media/*    # confirm MIME types

If a download returned HTML instead of media, the source is probably gated (private Drive, login wall, expired link). Ask the user once to either share a public URL or drop the file locally — then proceed.

Stage 2 — Analyze: understand what the asset is

You can't write good captions without knowing what the asset is about. Spend a minute on this — it's the difference between generic copy and copy that lands.

For video or audio:

Extract a transcript. The user likely has a transcript-extraction tool available (extracting-transcripts skill, WhisperX, AssemblyAI). If nothing's installed, use one of these:

# WhisperX (local, GPU-accelerated)
whisperx "asset.mp4" --output_format json --output_dir ./transcripts/

# faster-whisper (local CPU)
python -c "from faster_whisper import WhisperModel; m=WhisperModel('base'); segs,_=m.transcribe('asset.mp4'); [print(s.text) for s in segs]" > transcript.txt

# AssemblyAI (cloud, needs API key)
curl -X POST https://api.assemblyai.com/v2/transcript \
  -H "authorization: $ASSEMBLYAI_API_KEY" \
  -d "{\"audio_url\":\"$ASSET_URL\"}"

Read the transcript. What's the hook? What's the takeaway? What's the one sentence you'd put above the fold?

For images / carousels:

Look at each image (you have vision). Describe what you see, identify the narrative if it's a sequence (Steps / Mistakes / Before-After / Listicle), find the slide that's the strongest hook.

For URL-only posts (link share):

Read the page if it's reachable. Extract title, description, key claims.

Stage 3 — Draft: write the platform-tailored copy

Now you can draft. Each platform has different culture, character limits, and rhythm. Don't paste the same caption everywhere.

Draft for each target platform:

  • Caption / description / post body — adapted to the platform's voice and length
  • Title — for YouTube (under 100 chars)
  • Hashtags — count varies (Instagram 5-15, TikTok 3-5, LinkedIn 0-3, Twitter 1-2)
  • First comment — engagement question + CTA, for platforms that support it (YouTube, Instagram, LinkedIn, Twitter, Facebook)
  • Thumbnail — extract a strong frame from video, or use one the user provided

You don't fabricate facts. The captions reflect what the asset is actually about — drawn from the transcript, the image content, the user's stated angle. If the user said "post this with a take on X", your draft incorporates that angle. If they just said "post this," you draft from what the asset itself says.

Stage 4 — Convert: format conversions if needed

Different platforms need different shapes. Use ffmpeg.

# Carousel images → 9:16 video (4 sec per slide) for TikTok / Reels / Shorts
ffmpeg -framerate 1/4 -i "slide_%02d.jpg" -c:v libx264 \
  -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1" \
  -pix_fmt yuv420p -r 30 -y carousel-video.mp4

# 16:9 → 9:16 reframe (center-crop)
ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" -c:a copy -y output-9x16.mp4

# Extract a thumbnail at a specific timestamp
ffmpeg -i video.mp4 -ss 00:00:03 -vframes 1 -q:v 2 thumbnail.jpg

# Convert PNG → JPEG (Zernio's thumbnail upload requires JPEG)
ffmpeg -i thumb.png -q:v 2 thumb.jpg

# Resize image for thumbnail spec (YouTube 1280×720)
ffmpeg -i thumb.jpg -vf "scale=1280:720" -q:v 2 thumb-1280x720.jpg

Save converted files to ./media/ in the project. Use them in the upload step.

Stage 5 — Upload: get media into Zernio

Each media file needs a presigned upload, then a PUT, then a HEAD-verify. Zernio's REST API expects lowercase filename and contentType.

# 1. Presign
PRESIGN=$(curl -s -X POST "https://zernio.com/api/v1/media/presign" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"asset.mp4","contentType":"video/mp4"}')

UPLOAD_URL=$(echo "$PRESIGN" | jq -r .uploadUrl)
PUBLIC_URL=$(echo "$PRESIGN" | jq -r .publicUrl)

# 2. PUT (--tls-max 1.2 prevents TLS 1.3 bad_record_mac on macOS)
curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" \
  -T "./media/asset.mp4" --tls-max 1.2 \
  -s -o /dev/null -w "%{http_code}\n"

# 3. HEAD-verify (must be 200; 404 = silent upload failure → retry)
curl -s -o /dev/null -w "%{http_code}" -I "$PUBLIC_URL"

For files over 50 MB, the presigned URL path doesn't complete reliably. Upload to external storage instead (Google Drive public share, S3, Dropbox, etc.), get a public HTTPS URL, and pass that URL into the Zernio POST body's mediaItems[].url field. The Zernio post creates without an upload step in that case. See reference/zernio-upload.md § external-storage fallback for the specifics.

Repeat for the thumbnail. Thumbnails must be JPEG — convert PNGs first.

Stage 6 — Package: build the Zernio POST body

The POST body has a precise shape. Get these right; subtle violations silently break the post.

{
  "content": "Top-level caption — same starting point for every platform unless overridden per-platform",
  "mediaItems": [
    {
      "url": "https://...public-uploaded-or-external-url.mp4",
      "type": "video",
      "thumbnail": "https://...public-thumbnail.jpg"
    }
  ],
  "platforms": [
    {
      "platform": "youtube",
      "accountId": "",
      "platformSpecificData": {
        "title": "Video title",
        "visibility": "public",
        "tags": "claude,zernio,automation",
        "firstComment": "Engagement question + CTA link.",
        "categoryId": "28"
      }
    },
    {
      "platform": "instagram",
      "accountId": "",
      "platformSpecificData": {
        "visibility": "public",
        "firstComment": "Follow-up engagement hook"
      },
      "customContent": "Instagram-tailored caption (overrides top-level content for this platform)"
    }
  ],
  "scheduledFor": ""
}

The placement rules that matter:

  • content is the top-level caption / description for every platform. Per-platform overrides go in platforms[].customContent, not in platformSpecificData.description.
  • platformSpecificData is flat{title, tags, firstComment, ...} directly. Never {youtube: {title: ...}}.
  • tags is a comma-separated string: "a,b,c". Never an array ["a","b","c"].
  • mediaItems[].thumbnail is a plain URL string. Never {url: "..."}.
  • firstComment MUST be in the create call. It cannot be added via API after publish.
  • One media temp URL = one POST. If a post fails, re-upload before retry.

Resolve each platform's accountId at runtime:

curl -s "https://zernio.com/api/v1/accounts" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  | jq -r '.[] | "\(.platform): \(.id)"'

If a platform you're trying to post to isn't in the response, tell the user to connect it at https://zernio.com — that's a dashboard action, not an API one.

Stage 7 — Approve: show the user the full package

Present a single block. No clipping, no summaries — the user needs to see exactly what's about to ship.

## Ready to publish — review and confirm

Platforms:    youtube, linkedin, instagram, tiktok
Schedule:     2026-05-13T18:42:00Z  (3 min from now)
Media:        ./media/asset.mp4  →  uploaded ✓
Thumbnail:    ./media/thumb.jpg  →  uploaded ✓

— YouTube —
Title:        How I built my publishing system in a week
Description:  [first 5 lines]
Tags:         claude,zernio,automation,creator,workflow
Category:     Science & Technology (28)
First comment: What would you ship first? Drop it below.

— LinkedIn —
Caption:      [first 3 lines]
First comment: Repo's in the comments.

— Instagram —
Caption:      [first 3 lines]
Hashtags:     #claudecode #zernio #automation #creator #buildinpublic
First comment: Link in bio.

— TikTok —
Caption:      [first 3 lines]
Hashtags:     #claudecode #zernio #ai

Reply "ship it" / "post" / "approved" to publish, or tell me what to change.

Wait for an explicit affirmative. If they ask for changes, apply and re-show the full block. Never ship on a partial re-approval.

Stage 8 — POST: send to Zernio

curl -s -X POST "https://zernio.com/api/v1/posts" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$POST_BODY_JSON"

Capture the response. For each platform, extract postId, status, and url.

Stage 9 — Verify: did the post actually land?

After scheduledFor + 60s (retry onc

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.