Source profileQuality 85/100

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…

Best for

    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

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    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.

    Source-detected install commandSource
    npx skills add https://github.com/MoizIbnYousaf/marketing-cli --skill "skills/postiz"
    Safe inspection promptEditorial

    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

    1. 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.
    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…
    3. 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.
    4. 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.
    5. 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

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score85/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars27SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated 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:

    1. You never generate or rewrite copy.
    2. You never choose the platform mix — that's the user's ask or social-campaign's plan.
    3. 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.
    4. 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 from missing_envs rather than hardcoding. Canonical expectation: POSTIZ_API_KEY (required) and POSTIZ_API_BASE (defaults to https://api.postiz.com if unset; the catalog loader returns resolved_base: null in 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:

    PlatformPossible postiz identifier values
    LinkedInlinkedin (personal profile), linkedin-page (company page) — distinct integrations
    YouTubemay vary by postiz version (channel vs shorts flavors)
    Otherany 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):

    1. Look up the cached integrations from Step 2.
    2. Find all identifiers whose display name or known-alias matches the user's phrase. For LinkedIn, both linkedin and linkedin-page match.
    3. If exactly one matches → use it.
    4. If multiple match → present the options via AskUserQuestion. "You have both linkedin (personal) and linkedin-page (company) connected. Which?" Let the user pick; cache their choice in .mktg/cache/postiz-aliases.json for future sessions.
    5. 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.

    1. Run the On Activation steps above. Step 2b resolves any user-said platform names ("LinkedIn") to postiz identifiers (linkedin or linkedin-page or both).
    2. Build a single-item publish.json. The manifest's top-level name is the campaign identifier (the adapter threads this into sent-marker keys — do NOT also put campaign in metadata; the adapter ignores it):
      {
        "name": "ad-hoc-{timestamp}",
        "items": [{
          "type": "social",
          "adapter": "postiz",
          "content": "<the user's text verbatim>",
          "metadata": { "providers": ["linkedin", "bluesky"] }
        }]
      }
      
    3. mktg publish --adapter postiz --dry-run --input '<manifest>' --json — preview.
    4. On user confirm: mktg publish --adapter postiz --confirm --input '<manifest>' --json.
    5. 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".

    1. Run On Activation.
    2. 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.
    3. Build a multi-item publish.json with one item per (file × post). Each item's metadata.providers is a single-element array matching the file's platform.
    4. 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

    InputRequiredShapeNotes
    contentyesstringPlatform-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.providersyesstring[]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.mediaPathsnostring[]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 content are rejected via the base pipeline's rejectControlChars.
    • For each item, any identifier in providers that isn't in the cached /public/v1/integrations result produces a per-item failed result with detail: "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.providers fails per-item with detail: "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-PatternWhy it failsInstead
    Generate copy in this skillVoice 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-markerPostiz 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 skillEvery 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.providersintegration.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 adapterAlias 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 itemsRedundant: 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 v1v1 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 --confirm30 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 threadsTypefully 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

    LevelStateExperience
    L0No brand files, no postiz configuredActivation 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).
    L1POSTIZ_API_KEY set, POSTIZ_API_BASE defaulted to https://api.postiz.comThe 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.mdTone-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 chainEnd-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

    Computed 9438,313

    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 —

    Computed 89165

    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.

    Computed 95165

    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.

    Computed 9337,126

    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