User Manual · 05 · Playbooks — How To
WholeReach — Automated Marketing System

Playbooks — Every Task, Step by Step

The how-to heart of the manual: numbered, plain-English procedures for every operation — connecting accounts, approving posts, editing content, onboarding a client — each with the exact clicks and commands.

How to use this pageThis is the true how-to guide. Every operation you can perform is a numbered playbook below — first in plain English (what you're doing and why), then the exact steps, then the commands for whoever runs the server. You do not need to be technical to follow the plain-English steps. Two machines are involved: the hub (does the posting) at ssh root@64.227.29.192, and the main server (holds the website data) at ssh root@143.198.182.180. A line starting with ssh is typed into a terminal.
#PlaybookWhen you need it
The lifecycle of one postTo understand the whole flow first
1Connect a social accountAdding X, Bluesky, Mastodon, LinkedIn, or Meta
2Approve, edit, or skip a postRunning an approval-gated account
3Post something right nowYou want an immediate post
4Change how often it postsMore or fewer posts
5Add or remove a networkTurning a channel on/off for a tenant
6Change what a post saysEditing the wording/templates
7Add a new type of postA new content family
8Refresh the content nowPull fresh data immediately
9Preview what's queuedSee posts before they go out
10Stop everything instantlySomething looks wrong
11See what went outChecking results

The lifecycle of one post — and where you step in

Before the playbooks, here is the whole journey of a single post. Five stages run automatically on a schedule; the two green markers are where a human can step in.

Stage 1 · 05:00 · gather
The main server builds the feed. It reads your real data (site descriptions, marketing skills, score changes) and writes a clean list of publishable items. You can step in: edit the data sources or the feed builder (Playbook 8, Playbook 6).
Stage 2 · 05:10 · draft
The hub drafts the posts. It runs each feed item through a template to produce finished wording, and schedules each into an open time slot. If a required fact is missing, the post is held, never guessed.
Stage 3 · the gate
Approve — or auto-approve. If the tenant is set to require approval, drafts wait in the approval queue. You step in: approve, edit, or skip (Playbook 2). If the tenant is set to auto-fire, drafts skip this gate and are approved automatically.
Stage 4 · every 10 min · publish
The hub publishes what's due. Any approved post whose time has arrived is sent to its networks — within the X spend cap, and only if the kill switch is off.
Stage 5 · 05:20 · report
The hub logs results. What published, where, and early engagement — written to the logs and the digest.
The one thing to rememberNothing you do can make the system state an unverified fact. The worst case is a post is held and doesn't go out — never that it invents something. That is what makes it safe to run on autopilot and safe to run for a client.

Playbook 1 — Connect a social account

Every network is added the same way in spirit: authorize the account once, and the hub can post to it forever. Two kinds of networks: app-password ones (easiest) and developer-app ones (a one-time setup, sometimes an approval wait). Do the easy ones first.

Bluesky (easiest — 3 minutes, free)

In plain EnglishBluesky lets you create a one-time “app password” that you paste into the hub. No developer account, no approval.

Do it — step by step

  1. In a browser, sign in to bsky.app with the account you want to post from.
  2. Go to Settings → Privacy and security → App passwords, click Add app password, name it “WholeReach”, and copy the password it shows (you only see it once).
  3. Open the hub at postiz.wholetech.com and sign in.
  4. Click Add channel → Bluesky. Enter service https://bsky.social, your handle (e.g. walhus.bsky.social), and paste the app password. Click connect.
  5. Done — a test post confirms it. The channel now shows in the hub.

Under the hood (for the technical reader)

The hub stores the returned access token in the Integration table; the publisher reads it directly. Nothing to install.

Mastodon (easy — free)

In plain EnglishThe hub's Mastodon app is already registered. You just authorize your account through your Mastodon server.

Do it — step by step

  1. Sign in to the hub at postiz.wholetech.com.
  2. Click Add channel → Mastodon. Enter your instance URL (e.g. https://mastodon.social).
  3. You're bounced to Mastodon to log in and click Authorize. You return to the hub and the channel is connected.

X / Twitter (developer app — paid per post)

In plain EnglishX needs a one-time developer app so the hub can post on your behalf. X also charges per post now, so you load a small credit. Everything after the first setup is automatic.

Do it — step by step

  1. Go to console.x.com, sign in with the account you'll post from, and create a project + app. Set the app to Read and Write and type “Web App / Automated App or Bot”.
  2. In the app's User authentication settings, set the callback URL to https://postiz.wholetech.com/integrations/social/x.
  3. Copy the app's API Key and API Secret.
  4. Install the keys on the hub — run this in your own terminal and paste the keys when prompted (they never pass through anyone else):
    ssh -t root@64.227.29.192 /opt/postiz/set-x-keys.sh
  5. Back in the hub UI, click Add channel → X and authorize.
  6. Load credit: in the X developer portal, add funds (a $25 credit lasts months at our cap). See Playbook 11 for the spend cap.

Under the hood (for the technical reader)

Auth is OAuth 1.0a; keys live in /opt/postiz/social.env (root-only). The publisher signs each request with the stored token.

LinkedIn (developer app + review — free)

In plain EnglishSame idea as X, but LinkedIn reviews your app first (a few days). Start it early.

Do it — step by step

  1. Go to developer.linkedin.com, create an app, and link it to a LinkedIn Page you control.
  2. Request the Community Management API (page posting). This triggers LinkedIn's review.
  3. Set the callback to https://postiz.wholetech.com/integrations/social/linkedin.
  4. When approved, copy the Client ID + Secret and install them:
    ssh -t root@64.227.29.192 /opt/postiz/set-linkedin-keys.sh
  5. In the hub, Add channel → LinkedIn and authorize.

Facebook + Instagram (Meta app + business review — free)

In plain EnglishMeta is the most involved: one app covers both Facebook and Instagram, but it needs business verification and app review. Begin this first because the wait is longest.

Do it — step by step

  1. Go to developers.facebook.com, create an app (type “Business”), and add the Instagram Graph API and Pages products.
  2. Complete Business Verification and request the permissions to post to a Page and its linked Instagram professional account. Provide the privacy policy (automarketingengine.com/privacy/) and the data-deletion URL (the #data-deletion section on that page).
  3. Set the callback to https://postiz.wholetech.com/integrations/social/facebook.
  4. After approval, copy the App ID + Secret and install them:
    ssh -t root@64.227.29.192 /opt/postiz/set-meta-keys.sh
  5. In the hub, Add channel → Facebook, authorize, and pick the Page (and its linked Instagram account).

Under the hood (for the technical reader)

Plan the waitMeta and LinkedIn reviews can take several business days. Connecting Bluesky, Mastodon, and X gets you live immediately while those are in review.

Playbook 2 — Approve, edit, or skip a post

Approve, edit, or skip

In plain EnglishOn accounts set to require approval, drafts wait in a private queue. You (or the client) open a secure link on a phone and tap Approve, Edit, or Skip. Only approved posts ever publish.

Do it — step by step

  1. Open the approval link for the account (how to create one: Managing Clients). No password — the link itself is the key.
  2. You see a stack of cards, one per drafted post: the text, the network(s), and the scheduled time.
  3. For each card, tap Approve (it will publish at its time), Edit (change the wording, then approve), or Skip (it won't publish; you can note why).
  4. Held cards (missing a verified fact) are shown but can't be approved until the fact is supplied.
  5. That's it — approved posts publish automatically at their scheduled time.

Under the hood (for the technical reader)

The queue is served by portal_api.py (POST /portal-api/act with approve|edit|skip). Operators can also flip a post directly:

ssh root@64.227.29.192 "python3 -c \"import sqlite3;c=sqlite3.connect('/opt/portal/portal.db');c.execute('UPDATE posts SET state=1 WHERE id=123'.replace('1',chr(39)+'approved'+chr(39)));c.commit()\""
(Simpler: use the link. The publisher only ever sends posts in state approved.)

Playbook 3 — Post something right now

Publish immediately

In plain EnglishNormally posts go out at their scheduled slot. To push one out now, move its time to “now” and run the publisher.

Do it — step by step

  1. Find the post you want (see Playbook 9 to list the queue and get its ID).
  2. Set its time to now and make sure it's approved, then run the publisher:
    ssh root@64.227.29.192 "python3 - <<PY
    import sqlite3
    c=sqlite3.connect('/opt/portal/portal.db')
    c.execute("UPDATE posts SET state='approved', scheduled=strftime('%Y-%m-%dT%H:%M:00Z','now','-1 minute') WHERE id=123")
    c.commit()
    PY"
    ssh root@64.227.29.192 python3 /opt/portal/publish_due.py
    Replace 123 with your post's ID.
  3. The publisher prints the live URL(s) it created. Done.

Under the hood (for the technical reader)

publish_due.py selects state='approved' AND scheduled <= now, so setting the time in the past makes it eligible on the next run. The X daily cap and the PAUSE kill switch still apply.

Playbook 4 — Change how often it posts

Turn the posting rate up or down

In plain EnglishEach account has a “cadence” — how many posts it keeps in flight. Higher = more posts. Change one number, then regenerate.

Do it — step by step

  1. Open the tenant config on the hub:
    ssh root@64.227.29.192 nano /opt/portal/clients.json
  2. Find the account and change "cadence" (7 ≈ one a day; 14 ≈ two a day). Save (Ctrl+O, Enter) and exit (Ctrl+X).
  3. Rebuild the queue (safe to run anytime):
    ssh root@64.227.29.192 python3 /opt/portal/generate.py
  4. Check it with Playbook 9.

Under the hood (for the technical reader)

generate.py tops the queue up to cadence open posts, spread across the week's time slots, with least-recently-used rotation and a per-family cap for variety.

Playbook 5 — Add or remove a network for an account

Turn a channel on or off

In plain EnglishWhich networks an account posts to is a simple list. Add a name to post there; remove it to stop. Bluesky is free; X costs per post.

Do it — step by step

  1. Make sure the network is connected first (Playbook 1).
  2. Edit the tenant config:
    ssh root@64.227.29.192 nano /opt/portal/clients.json
  3. Set "channels" to the networks you want, e.g. ["x","bluesky"]. Drop "x" to go 100% free. Use ["dry"] to draft without sending anywhere (for testing). Save and exit.
  4. Regenerate so new drafts carry the new channels:
    ssh root@64.227.29.192 python3 /opt/portal/generate.py

Under the hood (for the technical reader)

Never point a test tenant at real channels. Use ["dry"] for anything experimental — it logs the post without sending it.

Playbook 6 — Change what a post says

Edit the wording (templates)

In plain EnglishPosts are written from a handful of sentence patterns called templates. Editing one changes how all posts of that type read. This is a small, safe text change.

Do it — step by step

  1. Open the templates file on the hub:
    ssh root@64.227.29.192 nano /opt/portal/portal_templates_wt.py
  2. Each template is one line in the TEMPLATES list, shaped like:
    (kind, family, id, required fields, how to write it)
    ("site", "site_feature", "sf1", ["blurb","url"],
     lambda l, b: _cap("%s %s" % (_trim(l["blurb"],210), l["url"])))
    Change the wording inside the final part. Keep the %s placeholders and the fields listed in “required fields”.
  3. Save and exit. Rebuild:
    ssh root@64.227.29.192 python3 /opt/portal/generate.py
  4. Preview the result (Playbook 9) before it goes out.

Under the hood (for the technical reader)

Templates are deterministic (no AI call), so output is predictable and costs nothing to generate. A field can only be used if it's in the item's data — that's the honesty guard.

Playbook 7 — Add a whole new type of post

Create a new content family

In plain EnglishA “type” of post (site feature, tip, milestone…) is a family. Adding one means telling the system both how to write it and where its facts come from.

Do it — step by step

  1. Add the writing pattern. In portal_templates_wt.py, add one or more template lines with a new kind and family, listing the fields they need.
  2. Supply the facts. In the feed builder on the main server (/root/gen-social-feed.py), add a section that reads your data and emits items tagged with that same kind and those fields.
  3. Rebuild the feed and the queue:
    ssh root@143.198.182.180 python3 /root/gen-social-feed.py
    ssh root@64.227.29.192 python3 /opt/portal/generate.py
  4. Preview (Playbook 9).

Under the hood (for the technical reader)

Keep the rule: emit an item only when every field its template needs is present. Missing field → the post is held, never guessed.

Playbook 8 — Refresh the content right now

Pull fresh data immediately

In plain EnglishThe feed refreshes itself nightly. To pull the latest data right now (new sites, new score wins), run the two builders by hand.

Do it — step by step

  1. Rebuild the feed on the main server (this also ships it to the hub):
    ssh root@143.198.182.180 python3 /root/gen-social-feed.py
  2. Rebuild the queue on the hub:
    ssh root@64.227.29.192 python3 /opt/portal/generate.py
  3. Preview (Playbook 9).

Playbook 9 — See what's queued before it publishes

List the upcoming posts

In plain EnglishYou can look at every drafted/approved post that hasn't gone out yet — the exact text, its network, and when it will publish.

Do it — step by step

  1. Run this on the hub to print the pending/approved queue:
    ssh root@64.227.29.192 "python3 - <<PY
    import sqlite3
    c=sqlite3.connect('/opt/portal/portal.db'); c.row_factory=sqlite3.Row
    for r in c.execute("SELECT id,client,state,substr(scheduled,1,16) s,text FROM posts WHERE state IN ('pending','approved') AND scheduled>=datetime('now') ORDER BY scheduled"):
        print('#%d [%s/%s] %s'%(r['id'],r['client'],r['state'],r['s'])); print('   ',r['text'][:130])
    PY"
  2. Each line shows the post ID (use it in other playbooks), the account, whether it's pending or approved, its time, and the text.

Playbook 10 — Stop everything instantly

The kill switch

In plain EnglishOne command freezes all publishing on every account until you turn it back on. Use it any time something looks wrong — it's instant and total.

Do it — step by step

  1. Freeze:
    ssh root@64.227.29.192 touch /opt/portal/PAUSE
  2. Nothing will publish while that file exists (the publisher checks for it first, every run).
  3. Resume when ready:
    ssh root@64.227.29.192 rm /opt/portal/PAUSE

Playbook 11 — See what went out (and manage X spend)

Check results

In plain EnglishEvery publish is logged with the live URL. You can read the log or the digest.

Do it — step by step

  1. See recent publishes and their URLs:
    ssh root@64.227.29.192 tail -n 40 /var/log/portal-pub.log
  2. See the nightly feed build:
    ssh root@143.198.182.180 tail /var/log/social-feed.log
  3. Failures also push a phone alert via ntfy, so you hear about problems without watching logs.

Manage X spend

In plain EnglishX is the only paid network (~1.5¢ a post, ~20¢ with a link). The system caps X at 3 posts a day so a small credit lasts months; Bluesky and the rest are free and uncapped.

Do it — step by step

  1. To change the cap, edit X_DAILY_CAP near the top of /opt/portal/publish_due.py on the hub.
  2. To post more to X, raise the cap and add funds in the X developer portal.
  3. To stop paying X entirely, remove "x" from a tenant's channels (Playbook 5) — Bluesky keeps posting free.