One social account of mine runs on an auto-posting bot. Five formats rotate on date rules: daily fortunes, app spotlights, guide articles, and a devlog thread three times a week. A few days ago I added one more thing on top — a growth prescription: until the account reaches 5,000 followers, publish two information posts and one direct follow request every day, as a priority.
Let me ask you something. When you add a new mode to a pipeline "as a priority", is that parallel or replacement? I believed it was parallel.
The bot was succeeding every single day
The logs were spotless. Three rows landed in the publishing ledger each day, the run summary said [end] ok, and the launchd exit code was 0. The fleet dashboard was green.
Today, while asking about something else, I counted published posts by type.
$ published, last 30 days
30 fortune
19 app spotlight
12 devlog thread
2 article/information <- prescription info posts
1 recruitment <- prescription follow requestThe 30 fortunes and 19 app posts were all from before the prescription. From the day after it turned on until today, exactly three posts shipped: two info, one recruitment. No fortunes. No app spotlights. No devlog threads.
The cause was one return at the top of a function
I opened the daily orchestrator.
def main():
...
if growth.active():
return run_growth(args, topic) # <- here
today = dt.date.today()
# Mon/Wed/Fri devlog thread
# fortune / app / guide single post
...growth.active() asks "are we still under 5,000 followers?" I'm at 1,387, so it is true on every run. Which means that since the day the prescription shipped, this bot has never once reached the rest of the schedule.
When I built the feature I wrote "apply the prescription as a priority", and the code translated that sentence into a return. The README even spelled it out: "while the prescription is active, prescription posts run instead of the existing rotation and the long devlog thread." It's unambiguous on the page. I wrote that sentence and still remembered the system as two tracks running side by side.
The green light wasn't lying. The bot did exactly what I told it to do. What I told it just wasn't what I thought I had told it.
How would you fix it?
There was a fork here.
- Drop the
returnand run the rotation after the prescription. - Split prescription and rotation into separate runs on separate schedules.
Option 2 looks cleaner, but both tracks share one account, one publishing ledger, and one duplicate-prevention lock. Splitting the runs means maintaining the lock and the ledger in two places. So I took option 1 — with three things attached.
if growth.active():
error = None
try:
run_growth(args, topic)
except (Exception, SystemExit) as e:
error = e # rotation still ships if the prescription breaks
print(f"prescription failed (rotation continues): {e}")
if not args.growth_only:
run_rotation(args, topic)
if error:
raise error # the failure still lands in the run summary
return
run_rotation(args, topic)- The rotation became its own function, so "this runs after the prescription" is visible in the code instead of implied.
- A failed prescription can no longer block the rotation. I have been burned by this exact shape before — one duplicate guard killed a whole day of posting once (the green-jobs, stale-site story is the same family of failure).
- A
--growth-onlyflag. A supervisor job calls the same script every 30 minutes to top up missing prescription posts. If the rotation rode along on those calls, fortunes and app spotlights would ship dozens of times a day. Only the top-up call passes the flag.
The third one matters most. The moment you delete an early return, every caller of that function inherits the rotation. When you fix a shape like this, count the callers before you edit.
Three self-checks
If you want to find the same bug in your own pipeline:
- What fraction of runs is your conditional early-exit true for? In my case
followers < 5,000was effectively permanently true. If a "temporary mode" condition stays true for months, it isn't temporary — it's the new default. - Are you counting output types, or only success? My dashboard watched "3 posts published". A per-type breakdown would have caught this in a day.
- Does your documentation contain the word "instead"? Mine did. That word was the spec, and I read past it every time.
The honest part
This bug cost me two days of posting. That's small. What's unsettling is how I found it: not by opening the code to fix this bot, but by counting a distribution while asking about something unrelated. Without that accident, the account would have gone months — until 5,000 followers at the current rate — with zero fortunes and zero app spotlights, quietly, with every log green.
Logs and exit codes answer "what succeeded". They never answer "what never ran at all". To see the second one you have to count the composition of your output, not the count of it.
Pick one automation you're running right now and break its last 30 days of output down by type. Not the total — the mix. Does it match what you expected?