MoizIbnYousaf/marketing-cli/skills/postiz/SKILL.md
postiz
Schedule social posts to any of 30+ providers via a running postiz instance (hosted at api.postiz.com or self-hosted). Use this skill whenever someone wants to post, schedule, or publish a draft to LinkedIn, Reddit, Bluesky, Mastodon, Threads, Instagram, TikTok, YouTube, Pinterest, Discord, Slack, or any non-Twitter social channel. ALWAYS chain content-atomizer first for platform-specific copy — this skill is a distribution layer only, it does not rewrite copy. For Twitter/X threads, use typeful
- Source repository stars
- 27
- Declared platforms
- 0
- Static risk flags
- 0
- Last source update
- 2026-07-28
- Source checked
- 2026-07-28
Decision brief
What it does—and where it fits
You schedule pre-written social posts to LinkedIn, Reddit, Bluesky, Mastodon, Threads, Instagram, TikTok, YouTube, Pinterest, Discord, Slack, and any other provider a running postiz instance supports. You do NOT write copy. You do NOT tailor per platform. Upstream skills (conten…
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/MoizIbnYousaf/marketing-cli --skill "skills/postiz"Inspect the Agent Skill "postiz" from https://github.com/MoizIbnYousaf/marketing-cli/blob/f12fbcbe4929584697b309b9096c9427b0cfce8e/skills/postiz/SKILL.md at commit f12fbcbe4929584697b309b9096c9427b0cfce8e. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Step 1 — Verify the catalog is registered and configured
catalog info returns the full CatalogEntry plus computed configured/missingenvs/resolvedbase fields. The command errors with exit code 1 (NOTFOUND) if the catalog is not in catalogs-manifest.json — that's the "not registered" signal.
Exit code 1 → postiz catalog is not registered. Tell the user: mktg catalog add postiz --confirm. Stop.configured: false → env vars are missing. Build the fix string from missingenvs rather than hardcoding. Canonical expectation: POSTIZAPIKEY (required) and POSTIZAPIBASE (defaults to https://api.postiz.com if unset; the…configured: true → proceed to Step 2. - 02
Step 2 — Resolve connected provider identifiers (cached)
Returns { adapter, integrations: [{ id, identifier, name, profile, disabled, picture, customer }, ...] } from GET /public/v1/integrations. Cache in .mktg/cache/postiz-integrations.json (TTL 15 minutes). Use this to validate the user's requested providers before publish — fail fa…
Returns { adapter, integrations: [{ id, identifier, name, profile, disabled, picture, customer }, ...] } from GET /public/v1/integrations. Cache in .mktg/cache/postiz-integrations.json (TTL 15 minutes). Use this to vali…Critical: identifier is a postiz-native provider-module key, NOT a platform display name. Single-variant providers collapse to the expected string ("reddit", "bluesky", "mastodon", "threads"), but some platforms ship mu…The adapter does exact-match on identifier (no alias layer) — if the user says "post to LinkedIn" and the instance only has linkedin-page connected, the adapter will refuse "linkedin". Alias handling is your job in Step… - 03
Step 2b — Handle user friendly-name aliasing (skill-layer only)
When the agent hears "post to LinkedIn" (a platform name, not a postiz identifier), map the friendly name to the right postiz identifier(s):
Look up the cached integrations from Step 2.Find all identifiers whose display name or known-alias matches the user's phrase. For LinkedIn, both linkedin and linkedin-page match.If exactly one matches → use it. - 04
Step 3 — Chain to content-atomizer FIRST if content is raw
If the user's input is a blog post, article, or long-form source (length 500 chars or file extension .md in marketing/content/), you MUST chain /content-atomizer first to produce platform-native copy. Feed the atomizer's output files (linkedin-posts.md, reddit-posts.md, etc.) in…
If the user's input is a blog post, article, or long-form source (length 500 chars or file extension .md in marketing/content/), you MUST chain /content-atomizer first to produce platform-native copy. Feed the atomizer'…If the user's input is already platform-tailored (short-form text targeting one platform), skip atomization and proceed. - 05
Step 4 — Tone-check against voice-profile (sanity guard only)
Read brand/voice-profile.md if it exists. Before building the publish manifest, verify each post: - Does the tone spectrum match (not wildly off-brand)? - Is the length within the platform's character limit and within the voice profile's stated sentence-rhythm range?
Does the tone spectrum match (not wildly off-brand)?Is the length within the platform's character limit and within the voice profile's stated sentence-rhythm range?Read brand/voice-profile.md if it exists. Before building the publish manifest, verify each post: - Does the tone spectrum match (not wildly off-brand)? - Is the length within the platform's character limit and within t…
Permission review
Static risk signals and limitations
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 85/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 27 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- MoizIbnYousaf/marketing-cli
- Skill path
- skills/postiz/SKILL.md
- Commit
- f12fbcbe4929584697b309b9096c9427b0cfce8e
- License
- MIT
- Collected
- 2026-07-28
- Default branch
- main
View the original SKILL.md
Postiz — Social Distribution Layer
You schedule pre-written social posts to LinkedIn, Reddit, Bluesky, Mastodon, Threads, Instagram, TikTok, YouTube, Pinterest, Discord, Slack, and any other provider a running postiz instance supports. You do NOT write copy. You do NOT tailor per platform. Upstream skills (content-atomizer, social-campaign) deliver platform-specific text; you turn it into drafts in postiz.
For Twitter/X threads, defer to typefully — its thread UX is canonical. You handle everything else.
North Star
Postiz is a distribution execution layer in the mktg playbook:
- You never generate or rewrite copy.
- You never choose the platform mix — that's the user's ask or social-campaign's plan.
- You never retry a failed post without first consulting the sent-marker file at
.mktg/publish/{campaign}-postiz.json— postiz has no idempotency key, and retries duplicate. - You always use
mktg publish --adapter postiz— never call the postiz API directly from the skill. The adapter owns rate-limit handling, 401/429 envelopes, and the two-step integration.id resolution.
On Activation
Run these 4 steps before anything else. Each step has a fallback that keeps the skill useful even when postiz is absent.
Step 1 — Verify the catalog is registered and configured
mktg catalog info postiz --json --fields configured,missing_envs,auth.credential_envs,resolved_base
catalog info <name> returns the full CatalogEntry plus computed configured/missing_envs/resolved_base fields. The command errors with exit code 1 (NOT_FOUND) if the catalog is not in catalogs-manifest.json — that's the "not registered" signal.
- Exit code 1 → postiz catalog is not registered. Tell the user:
mktg catalog add postiz --confirm. Stop. configured: false→ env vars are missing. Build the fix string frommissing_envsrather than hardcoding. Canonical expectation:POSTIZ_API_KEY(required) andPOSTIZ_API_BASE(defaults tohttps://api.postiz.comif unset; the catalog loader returnsresolved_base: nullin that case and the skill applies the default). Stop.configured: true→ proceed to Step 2.
Step 2 — Resolve connected provider identifiers (cached)
mktg publish --adapter postiz --list-integrations --json
Returns { adapter, integrations: [{ id, identifier, name, profile, disabled, picture, customer }, ...] } from GET /public/v1/integrations. Cache in .mktg/cache/postiz-integrations.json (TTL 15 minutes). Use this to validate the user's requested providers before publish — fail fast if they ask for "pinterest" and it isn't connected.
Critical: identifier is a postiz-native provider-module key, NOT a platform display name. Single-variant providers collapse to the expected string ("reddit", "bluesky", "mastodon", "threads"), but some platforms ship multiple identifiers:
| Platform | Possible postiz identifier values |
|---|---|
linkedin (personal profile), linkedin-page (company page) — distinct integrations | |
| YouTube | may vary by postiz version (channel vs shorts flavors) |
| Other | any string postiz's provider module exports |
The adapter does exact-match on identifier (no alias layer) — if the user says "post to LinkedIn" and the instance only has linkedin-page connected, the adapter will refuse "linkedin". Alias handling is your job in Step 2b.
Step 2b — Handle user friendly-name aliasing (skill-layer only)
When the agent hears "post to LinkedIn" (a platform name, not a postiz identifier), map the friendly name to the right postiz identifier(s):
- Look up the cached integrations from Step 2.
- Find all identifiers whose display name or known-alias matches the user's phrase. For LinkedIn, both
linkedinandlinkedin-pagematch. - If exactly one matches → use it.
- If multiple match → present the options via AskUserQuestion. "You have both
linkedin(personal) andlinkedin-page(company) connected. Which?" Let the user pick; cache their choice in.mktg/cache/postiz-aliases.jsonfor future sessions. - If zero match → error: "'LinkedIn' is not connected in this postiz instance. Connected: {enumeration}. Connect it in the postiz UI first."
Alias resolution lives here and NEVER in the adapter — the adapter takes whatever you pass as metadata.providers and resolves against the live integrations verbatim.
Step 3 — Chain to content-atomizer FIRST if content is raw
If the user's input is a blog post, article, or long-form source (length > 500 chars or file extension .md in marketing/content/), you MUST chain /content-atomizer first to produce platform-native copy. Feed the atomizer's output files (linkedin-posts.md, reddit-posts.md, etc.) into this skill.
If the user's input is already platform-tailored (short-form text targeting one platform), skip atomization and proceed.
Step 4 — Tone-check against voice-profile (sanity guard only)
Read brand/voice-profile.md if it exists. Before building the publish manifest, verify each post:
- Does the tone spectrum match (not wildly off-brand)?
- Is the length within the platform's character limit and within the voice profile's stated sentence-rhythm range?
If a check fails, surface a warning to the user and ask before proceeding. Never rewrite. That's content-atomizer's job — bounce back there.
How to Use
This skill runs in one of two modes. Pick by looking at the input.
Mode A — Single post to one or more platforms
User says: "Post this to LinkedIn and Bluesky" + paste text.
- Run the On Activation steps above. Step 2b resolves any user-said platform names ("LinkedIn") to postiz identifiers (
linkedinorlinkedin-pageor both). - Build a single-item
publish.json. The manifest's top-levelnameis the campaign identifier (the adapter threads this into sent-marker keys — do NOT also putcampaigninmetadata; the adapter ignores it):{ "name": "ad-hoc-{timestamp}", "items": [{ "type": "social", "adapter": "postiz", "content": "<the user's text verbatim>", "metadata": { "providers": ["linkedin", "bluesky"] } }] } mktg publish --adapter postiz --dry-run --input '<manifest>' --json— preview.- On user confirm:
mktg publish --adapter postiz --confirm --input '<manifest>' --json. - Report per-provider status from the adapter's response envelope.
Mode B — Campaign from content-atomizer output
User says: "Schedule my atomized blog post to LinkedIn, Reddit, and Threads".
- Run On Activation.
- Read
marketing/social/{source-slug}/linkedin-posts.md,reddit-posts.md,threads-posts.md. Each file contains one or more posts with YAML frontmatter matching the atomizer's contract. - Build a multi-item
publish.jsonwith one item per (file × post). Each item'smetadata.providersis a single-element array matching the file's platform. - Same dry-run → confirm → report flow as Mode A.
Example 1 — LinkedIn-only draft
mktg publish --adapter postiz --confirm --input '{
"name": "linkedin-announcement",
"items": [{
"type": "social",
"adapter": "postiz",
"content": "We shipped v2.0 today. Here is what changed...",
"metadata": { "providers": ["linkedin"] }
}]
}' --json
Example 2 — Cross-platform (atomizer output + postiz distribution)
/content-atomizer marketing/content/blog/v2-announcement.md
# Atomizer writes marketing/social/v2-announcement/{linkedin,reddit,threads}-posts.md
# Build a publish.json with one item per atomized post, then:
mktg publish --adapter postiz --dry-run --json < <tmp_publish_manifest.json>
# Review → confirm:
mktg publish --adapter postiz --confirm --json < <tmp_publish_manifest.json>
Example 3 — Mixed batch (postiz + typefully)
This is the social-campaign Phase 5 path. You do NOT handle this standalone — the orchestrator in social-campaign routes per-post per the Phase 5 routing table.
Inputs
| Input | Required | Shape | Notes |
|---|---|---|---|
content | yes | string | Platform-tailored post text. Must fit the target platform's character limit (LinkedIn 3000, Reddit no limit, Bluesky 300, Mastodon 500, Threads 500). Failing this → Step 4 tone-check surfaces a warning. |
metadata.providers | yes | string[] | Postiz identifiers, not platform display names. Canonical examples: "linkedin" (personal), "linkedin-page" (company page), "reddit", "bluesky", "mastodon", "threads", "instagram", "tiktok", "youtube", "pinterest", "discord", "slack". Must match identifier exactly from GET /public/v1/integrations. Unknown identifiers fail fast per-item with the connected list in the error. Alias resolution (user says "LinkedIn" → you pick linkedin vs linkedin-page) happens in Step 2b, never in the adapter. |
metadata.mediaPaths | no | string[] | v2 stub — ignored in v1 (draft-only, images hardcoded to [] in the adapter). Don't populate this field in v1 manifests; it has no effect. |
Campaign identifier does NOT live in metadata. The adapter reads it from the publish manifest's top-level name field. If you set metadata.campaign, the adapter silently ignores it. Never emit metadata.campaign.
Validation performed at publish-time, before the network:
- Control chars in
contentare rejected via the base pipeline'srejectControlChars. - For each item, any identifier in
providersthat isn't in the cached/public/v1/integrationsresult produces a per-itemfailedresult withdetail: "Unconnected provider(s): X. Connected: {list}. Connect in the postiz UI first.". The adapter does NOT short-circuit the whole batch — items with fully-connected providers still publish. - Missing
metadata.providersfails per-item withdetail: "Missing item.metadata.providers[] — add at least one postiz identifier".
Outputs
The adapter returns the standard AdapterResult envelope:
{
"adapter": "postiz",
"items": 3,
"published": 3,
"failed": 0,
"errors": [],
"results": [
{ "item": 0, "status": "published", "detail": "draft id=abc123 provider=linkedin" },
{ "item": 0, "status": "published", "detail": "draft id=abc124 provider=reddit" },
{ "item": 0, "status": "skipped", "detail": "dedupe: sent-marker match" }
]
}
Per-provider fan-out: one input item with N providers becomes N entries in results[], each with its own status. A partial failure (Reddit rate-limited, LinkedIn succeeded) does NOT fail the whole item.
Exit-code aggregation: the top-level publish command exits 0 whenever at least one item succeeds. Top-level non-zero exit only when ZERO items succeed. This matches how typefully and resend adapters already behave. Inspect results[] for per-item outcomes — a successful publish run can still have failed providers inside it.
Sent-marker side-effect: successful publishes append to .mktg/publish/<campaign>-postiz.json with a stable hash key. Re-running the same campaign is a no-op for already-sent items.
Platform coverage table (UI hint): after a run, render a small confirmation table showing per-provider status, draft ID, and notes.
Anti-Patterns
| Anti-Pattern | Why it fails | Instead |
|---|---|---|
| Generate copy in this skill | Voice drift — each skill that rewrites copy diverges from voice-profile.md. We have one generation path (content-atomizer) for a reason: platform-specific copy rules live there. If you inline paraphrasing here, the voice profile's guardrails don't fire. | Chain /content-atomizer first. Feed its file output into this skill. |
| Retry a failed post without checking the sent-marker | Postiz has no idempotency key and no webhooks. A retry without checking .mktg/publish/<campaign>-postiz.json duplicates the draft — if the user accepted the first one before the adapter crashed, you just ship the post twice. | Always call the adapter. The adapter consults the sent-marker before POSTing. Never construct your own HTTP request. |
| Call the postiz API from within this skill | Every HTTP caller reinvents error handling. The adapter owns 401 (bad key), 429 (rate limit with Retry-After), 400 (bad provider), and network timeouts — each with structured fix fields. Skipping the adapter loses these. | mktg publish --adapter postiz .... Always. |
Pass postiz integration.id DB primary keys in metadata.providers | integration.id values are database rows — they change between environments (hosted vs self-host vs fresh install). A hardcoded "42" breaks the moment the user rotates their instance. | Pass postiz identifiers ("linkedin", "linkedin-page", "bluesky") verbatim — these are provider-module keys that are stable across instances. The adapter does the identifier → id lookup. |
| Translate user-said platform names to identifiers in the adapter | Alias drift: the moment postiz ships a new provider module, any in-adapter alias layer becomes wrong. The adapter does exact-match on identifier; aliasing is a SKILL-layer concern. | Handle alias resolution in Step 2b of On Activation (this skill). Pass exact postiz identifiers to the adapter. |
Populate metadata.campaign in publish items | Redundant: the adapter threads manifest.name into sent-marker keys. If you also set metadata.campaign, the adapter ignores it — confusion for the agent about which wins. | Use the top-level manifest.name field as the campaign identifier. Don't duplicate into metadata. |
Populate metadata.mediaPaths in v1 | v1 is draft-only; the adapter hardcodes images to []. Setting mediaPaths silently no-ops and wastes tokens. | Leave mediaPaths out of v1 manifests. Image support lands in v2 alongside schedule / now post types. |
Skip dry-run and go straight to --confirm | 30 drafts/hour is a tight rate limit; burning quota on broken manifests is wasteful. Dry-run validates providers and env vars without touching the network. | Always dry-run first. |
| Use this skill for X threads | Typefully owns threads — better UX, per-tweet character validation, and thread-unroll view. Postiz can post to X but thread authoring is weak. | Twitter/X single post OR thread → typefully. Everything else → this skill. |
Progressive Enhancement
| Level | State | Experience |
|---|---|---|
| L0 | No brand files, no postiz configured | Activation step 1 fails with configured: false and emits a clean fix: set POSTIZ_API_KEY and optionally POSTIZ_API_BASE. No network calls, no partial state, no crash. Exit code 3 (dependency missing). |
| L1 | POSTIZ_API_KEY set, POSTIZ_API_BASE defaulted to https://api.postiz.com | The skill works for every platform the hosted instance supports. voice-profile.md absent → tone-check is a no-op. User feeds raw copy → warning suggests chaining content-atomizer but doesn't block. |
| L2 | + voice-profile.md | Tone-check fires. Posts that violate length or tone spectrum surface warnings before the publish confirm prompt. Still functional without atomization. |
| L3 | + content-atomizer has run and written marketing/social/{slug}/ | Mode B is available. Batch-publish N atomized posts across M platforms with one confirm. |
| L4 | + full brand + postiz configured + content-atomizer chain | End-to-end: user says "launch my blog post on LinkedIn/Reddit/Bluesky", orchestrator routes, atomizer writes, postiz publishes. Voice honored; sent-markers prevent dupes; partial failures roll forward. |
This skill never fails silently. At every level, the user either gets structured output (publish succeeded/failed) or a structured error with a fix field. Absent postiz, the skill errors cleanly — no half-finished drafts.
Related Skills
- content-atomizer — generates platform-specific copy. MUST run before this skill for long-form source content.
- typefully — X/Twitter thread handling. Complements this skill (X threads → typefully; everything else → this skill).
- social-campaign — orchestrator that chains strategy/write/review/design/schedule. In Phase 5, it picks the scheduling backend per platform.
- brand-voice — writes
voice-profile.md, which this skill reads as a sanity guard in Step 4.
Alternatives
Compare before choosing
wshobson/agents
brand-landingpage
Brand-first landing page designer — runs a brand-identity interview (colors, typography, shape language), then generates and iterates on a polished landing page via Stitch with deployment-ready HTML. Use when the user asks to create, design, or build a landing page, homepage, or marketing page and has no established visual direction. Skip when they have a design mockup, need a dashboard or app UI, are working at component level, building a multi-page app, or restyling with known design tokens —
JasonColapietro/suede-creator-skills
suede-code-review
Find the bugs a diff can actually ship: TypeScript, React, Next.js, OWASP, accessibility, SEO, database, and deploy-risk review. Return findings, not a grade.
JasonColapietro/suede-creator-skills
suede-code-grader
Give a blunt A-F ship grade for a code change across correctness, security, data, UX, verification, and deploy readiness. Use for a grade, not a findings review.
github/awesome-copilot
flowstudio-power-automate-build
Build, scaffold, and deploy Power Automate cloud flows using the FlowStudio MCP server. Your agent constructs flow definitions, wires connections, deploys, and tests — all via MCP without opening the portal. Load this skill when asked to: create a flow, build a new flow, deploy a flow definition, scaffold a Power Automate workflow, construct a flow JSON, update an existing flow's actions, patch a flow definition, add actions to a flow, wire up connections, or generate a workflow definition from