Tracking scheduled Medium posts without a browser extension
I’ve been building blog-manager to handle syndication across Medium and LinkedIn. The Medium integration started simple: posts are either not_imported, draft, or published. That covered everything I needed until I started scheduling posts in Medium, setting them to publish at a future date. Medium doesn’t expose that via its API, so my app had no idea a post was in a scheduled state.
The fix is Phase 1 of a two-phase approach: model the state in the app, let me set it manually, and surface it in the UI. Phase 2 would automate detection. But before building any automation, I wanted to know: does the manual version actually create enough friction to justify the complexity? So I built just the data model and UI first.
What I added
The core change is a new enum value. Rails makes this straightforward but there’s one rule worth calling out: enum integer values must be stable. If you ever change the integer behind an existing key, say because you inserted a value in the middle, every row in your database silently changes meaning. So I always append:
enum :medium_status, { not_imported: 0, draft: 1, published: 2, scheduled: 3 }, prefix: :medium
I wrote a test that asserts the exact hash, not just that the new value works:
test "medium_status integer mappings are stable" do
assert_equal({ "not_imported" => 0, "draft" => 1, "published" => 2, "scheduled" => 3 },
Post.medium_statuses)
end
That test would catch any future reordering immediately.
The migration just adds a nullable medium_scheduled_at datetime column. No default, no constraint. The date is optional because sometimes I schedule something in Medium without noting the exact time.
For the UI, three things needed updating:
The status badge partial already had a preset map keyed by enum string value. Adding "scheduled" to the map was a one-liner. It picks up the status-soon class (yellow), same as draft, which feels right; both are “not yet published” states.
The posts index filter dropdown auto-populates from Post.medium_statuses.each_key, so scheduled appeared there for free once the enum was updated. Zero explicit changes needed to the filter.
The dashboard got a new “Upcoming on Medium” widget. It queries Post.kept.where(medium_status: :scheduled).includes(:blog).order(medium_scheduled_at: :asc) and renders only when there are results. Posts without a scheduled date show a dash.
What I decided not to build
The issue originally included an edit form so I could fill in medium_scheduled_at manually. I cut it. The whole point of this phase is to set up the data model so Phase 2 automation can write to it. If I need to test manually, a rails runner one-liner is fine. An edit form is real scope and I’d rather validate that the status shows up correctly before building the editing UI.
What surprised me
The filter just worked. I assumed I’d need to explicitly add scheduled to the dropdown select options, but since the view iterates Post.medium_statuses.each_key, adding the enum value was all it took. Rails enums are surprisingly nice about this.
What’s next
Phase 2 is about automating detection, either by polling Medium’s RSS feed or parsing emails. That turned into the browser extension instead. For now I can at least mark posts scheduled manually and they’ll show up on the dashboard.
Related reading
Building a blog syndication backfill
72 Medium posts my database knew nothing about: the RSS cap, an undocumented GraphQL workaround, Unicode apostrophes, and the kamal stdin trap.
Building Medium cross-posting for blog-manager
A push-to-Medium button on a deprecated-but-working API: injectable Net::HTTP, on-demand markdown rendering, and a state machine split between job and service.
The last native publisher: finishing the Postiz consolidation
Medium's native integration was a museum exhibit. A browser bridge, an article whose body is a URL, and minus 1,603 lines.