How the schedule fires on time
Once a day, at 7am local time, an EventBridge Scheduler rule fires the scheduler Lambda. The Lambda reads the calendar, looks at one approved post at a time, works out when it’s due, and decides whether to leave it alone or to book it for sending — and if it’s already due, hand it off. The whole decision is plain Python. No model. No guesswork. Every posting window lives in the rules doc, where the owner can edit it without a deploy.
Key takeaways
- The scheduler runs once a day via EventBridge Scheduler at 7am local time.
- Posting windows live in the rules doc — default is no posts before 8am or after 8pm, plus blackout days.
- Four moves per post, every tick: resting, queue, send, retry.
- DynamoDB tracks each post’s status so a post is never double-sent.
- The scheduler itself never calls a model. The decision is entirely deterministic.
The decision flow, per post
Posting windows: 8am to 8pm isn’t magic, it’s in the doc
The rules doc has one short section for the posting windows. It names them in plain prose: “Post only between 8am and 8pm local. No posts on Sundays. Skip these blackout dates: Dec 25, Jan 1.” The window is when posts are allowed to leave. A post scheduled for 2am isn’t sent at 2am; the scheduler nudges it to the first allowed minute — 8am the same morning, or the next non-blackout day if the whole day is blocked.
The windows exist for a reason. A post that lands at 3am gets buried by morning and barely seen. A post on a blackout day — a holiday you’re closed for, a day you’d rather stay quiet — is one you scheduled by mistake or want to skip. Different businesses have different rhythms; the windows reflect that.
Per-post overrides exist too. The sheet has an optional column called send_exact. Set it to yes and the scheduler honors the exact minute you typed, even outside the normal window — the right escape hatch for the time-sensitive announcement you want out at 6am sharp.
Four moves, always
Every post, every tick, lands in exactly one of four buckets. The names are simple on purpose.
- Resting. The post is more than a day away, or it hasn’t been approved, or it’s already gone out. Do nothing. Most posts, most ticks, are resting.
- Queue. The post is approved and due today but the minute hasn’t arrived. Book a one-off EventBridge Scheduler rule for the exact send minute. Write a row to the
ss-statusDynamoDB table marking the post as queued so the next tick doesn’t book it twice. - Send. The send minute has arrived. Hand the post to the Sender, which does the format check and the actual posting (Part 4). Mark the post as sending in DynamoDB.
- Retry. A channel failed on a previous attempt — the platform was briefly down, the token needed a refresh — and the retry budget for that post isn’t spent. Queue it again after a short delay (a few minutes, growing on each attempt, up to a small cap). When the budget runs out, the post is marked failed and the owner is told. Most sends never need a retry; the budget is there for the rare flake.
State that makes the decision deterministic
The scheduler reads two DynamoDB tables every tick. ss-status records each post’s current state: (post_id, state, queued_for) where state is one of draft, queued, sending, sent, or failed. ss-sends records every actual send attempt: (post_id, channel, attempt, result, posted_url_or_error). With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given post with a given scheduled time, a given approval flag, and a given send history always produces the same move. Re-running the tick produces no extra posts (because the state in DynamoDB shows what already fired).
Holding a post is an explicit reset: its state drops back to draft and any queued one-off send is cancelled. Sending it now is the opposite: the state jumps straight to sending. Part 5 covers those actions in detail.
Why the daily tick uses no model
The scheduler could call a model on the tick to pick a “better” time to post, or to decide whether a post is worth sending at all. It doesn’t. Two reasons. First, the daily tick should be the one part of the system that is utterly predictable — if the sheet says post at 9am and it’s approved, it posts at 9am. A model in that loop introduces variance the owner can’t reason about. Second, model calls cost money, and most posts most ticks are resting, so the call would be wasted nearly every time.
Bedrock fires elsewhere — on the draft-helper lane in Part 2, and on the monthly recap mentioned in Part 6. Not on the daily tick. The scheduler itself is plain Python that reads a calendar and books sends.
Next post: how a post gets formatted per channel, how the format checks run, and what the Sender does when a check fails.
All posts