ContentOpen SourceFreeActiveMachine-verified· intermediate · ~20 min setup

Postiz: Plan and Schedule a Week of Social Posts

From one brief, have an agent draft per-channel posts and queue a non-colliding weekly schedule in self-hosted Postiz.

by Shilpa Mitra· verified today· v1.0.0

Run this workflow

CI-verified, 5/5 fixtures passing.

Intended Use

Anyone who wants an agent to turn one campaign brief into a week of per-channel posts, queued on a schedule in self-hosted Postiz, instead of a paid tool like Buffer. CI verifies the deterministic spine: the plan the agent produces is a valid weekly schedule. Drafting the copy and posting to the live platforms is fenced (needs a model and your per-network API keys).

Not for

  • The live platform posting, which needs a developer app + API keys per network
  • Expecting CI to verify your X/LinkedIn/Instagram connections, that part is credential-gated and fenced
  • Anyone who won't run Docker to self-host

Replaces Buffer

The Stack

Tested Against

postiz-app@latestnode@20.x

Side effects & data flow

Network
none, local only
Writes
./plan.json, ./checkpostiz.mjs
Credentials
none required

Prerequisites

  • Docker (Postiz self-hosts via docker compose; dashboard at http://localhost:5000)
  • A developer app + API keys for each social network you connect
  • Node 20+

Steps

  1. 1

    Self-host Postiz and connect your channels

    Bring Postiz up with Docker (docker compose up -d; dashboard at http://localhost:5000), then spend your setup time creating a developer app and pasting API keys for each network you connect. This part is infrastructure, not something CI runs.

  2. 2

    Draft the weekly plan (the structured output an agent emits)

    An agent turns your brief into a structured plan: one entry per post with a channel, the copy, and a scheduled time. This is a representative plan; CI validates its shape next.

    cat > plan.json <<'EOF'
    [
      { "channel": "x",         "content": "Launch day: our open-source scheduler is live. One prompt, a whole week of posts.", "scheduledAt": "2027-01-04T09:00:00Z" },
      { "channel": "linkedin",  "content": "We shipped an agentic, self-hostable alternative to Buffer. Here is what changed and why.", "scheduledAt": "2027-01-04T15:00:00Z" },
      { "channel": "instagram", "content": "Behind the scenes of launch week.", "scheduledAt": "2027-01-05T12:00:00Z" },
      { "channel": "x",         "content": "Day two: how the agent drafts per-channel copy from a single brief.", "scheduledAt": "2027-01-06T09:00:00Z" },
      { "channel": "linkedin",  "content": "A short note on self-hosting your social stack instead of renting it.", "scheduledAt": "2027-01-07T15:00:00Z" }
    ]
    EOF
    test -s plan.json && echo "plan drafted: 5 posts"
  3. 3

    What CI checks: the plan is a valid, non-colliding weekly schedule

    CI validates the deterministic contract between the agent and Postiz: every channel is supported, X posts fit 280 chars, no two posts share a channel+time slot, the posts spread across 3+ days, and the schedule is chronologically ordered. A malformed plan would never queue. No model, no keys.

    cat > checkpostiz.mjs <<'EOF'
    import { readFileSync } from 'node:fs';
    const plan = JSON.parse(readFileSync('plan.json', 'utf8'));
    const CHANNELS = new Set(['x','linkedin','instagram','facebook','threads','mastodon','bluesky','tiktok','youtube','pinterest']);
    let ok = true;
    function check(label, cond){ console.log(label + ': ' + (cond ? 'yes' : 'NO')); if(!cond) ok=false; }
    check('plan is a non-empty array', Array.isArray(plan) && plan.length > 0);
    check('every channel is supported', plan.every(p => CHANNELS.has(p.channel)));
    check('every post has content', plan.every(p => typeof p.content === 'string' && p.content.trim().length > 0));
    check('x posts within 280 chars', plan.every(p => p.channel !== 'x' || p.content.length <= 280));
    const times = plan.map(p => Date.parse(p.scheduledAt));
    check('every scheduledAt is a valid ISO time', times.every(t => !Number.isNaN(t)));
    const slots = plan.map(p => p.channel + '@' + p.scheduledAt);
    check('no two posts collide on a channel+slot', new Set(slots).size === slots.length);
    const days = new Set(plan.map(p => p.scheduledAt.slice(0, 10)));
    check('posts spread across 3+ days', days.size >= 3);
    check('plan is chronologically ordered', times.every((t, i) => i === 0 || t >= times[i - 1]));
    if(!ok){ console.log('postiz plan check FAILED'); process.exit(1); }
    console.log('postiz plan check OK');
    EOF
    node checkpostiz.mjs
  4. 4

    Let it queue and post (the model + platform step, not checked by CI)

    From your brief, the agent drafts the real per-channel copy and Postiz queues and publishes it on the schedule. Both the drafting (a model) and the posting (live platform APIs with your keys) are non-deterministic and credential-gated, so CI never runs them. The badge covers the schedule contract, not the live posts.

Eval, 5 fixtures

Last passed: verified today
  • channels-validcontainstimeout 30s · max $0

    Expected: every channel is supported: yes

  • within-limitscontainstimeout 30s · max $0

    Expected: x posts within 280 chars: yes

  • no-collisioncontainstimeout 30s · max $0

    Expected: no two posts collide on a channel+slot: yes

  • plan-okcontainstimeout 30s · max $0

    Expected: postiz plan check OK

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 0

Results

~31K stars; the self-hosted, agentic alternative to Buffer and Hootsuite.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).