On 2026-09-12 I rescheduled 38 articles on this site with a single SQL statement. The JSON:API showed the new schedule straight away, and it kept showing it for 17 days. The Scheduler module, which is the thing that actually publishes them, never saw the change. It kept publishing on the old schedule the whole time.
Why I used SQL
The articles were nodes 225 to 262, all drafted and scheduled ahead of time, one per day. Reviewing the queue, I didn't like the order. A multi-part series about one Postgres extension was scheduled out of its own chronological order, and the month came in two long blocks of one topic each. I wanted to reorder everything, interleave the topics, and spread publish times across the evening instead of everything going out at the same minute.
Re-saving 38 nodes one by one through the API felt heavy for what was just a date change. The publish date is a field, publish_on. Before writing the update, I checked whether Scheduler kept its own table. It doesn't: there's no scheduler_node table, the date lives on the node itself. So I updated node_field_data.publish_on directly, read the queue back through JSON:API, and saw exactly the plan I wanted.
Two tables, one field
What I missed is that nodes in Drupal are revisionable, so every revisionable field lives in two places. node_field_data holds the current values, which is what an ordinary entity load reads and what JSON:API returns. node_field_revision holds the value per revision. Normally the two agree, because saving a node through the entity API writes both. My SQL wrote one.
Scheduler 2.3.0 finds content that's due with an entity query, and for revisionable entity types it adds one line:
$query->latestRevision();
The comment above it explains why: it makes Scheduler work correctly with content moderation, where the latest revision isn't necessarily the published one. The effect for me was that Scheduler's query read publish_on from the revision table, and the revision table still held the original schedule: one article per day, at 20:00 local time.
The evidence was in plain sight
Nodes 230 to 241 each went live at exactly 20:00:00 local time, on consecutive days. This site has Scheduler set to update a node's created date on publish, so the published timestamps are a precise record of when each publish happened. Not one of them landed at the randomized times I'd planned. Meanwhile, a handful of nodes whose new dates had already passed sat unpublished, looking overdue in JSON:API while being perfectly on time according to the table Scheduler actually reads.
My first theory was caching: maybe Scheduler or some entity cache was holding stale values. I ran drush cr and then drush cron. Nothing changed. That was useful, because it ruled out caching and pointed back at the data itself. Comparing the two tables side by side showed the mismatch immediately.
The repair
On 2026-09-29 at about 21:11, I copied the planned dates from node_field_data into node_field_revision for the unpublished nodes, 241 to 262, after saving the old revision values to a backup file on the server. The next cron run published the overdue articles, and the rest of the queue has followed the plan since. A query comparing the two tables for every unpublished node now finds zero differences.
Some of it couldn't be undone. Nodes 225 to 240 had already gone out on the old dates and in the old order. Node 241 had published at 20:00 that same evening, on the old schedule, before the fix. It had been planned for late October.
What I'd do differently
The narrow lesson: if you change a field on a revisionable entity with SQL, change it in every table that stores it. In Drupal that means the data table and the revision table, and possibly more if the field has its own storage tables.
The better lesson is not to use SQL for this at all. Loading each node and saving it through the entity API is slower to write and slower to run, and it would have been correct automatically, because the entity system knows which tables a field lives in and I didn't. Using SQL made the change quick to make and hard to verify, and the one check I ran, reading back through JSON:API, read the same table I'd just written. It could only ever confirm my change.
That's the part that bothers me most. I did verify the change. I just verified it against the table I'd written, not the one Scheduler reads.
Leave a comment
Comments are reviewed before they appear.