ddtcorex

14 Oct 2026 · 6 min read

Scheduled content updates without Commerce staging

Using cron-driven scheduled imports and catalog_product_entity updates to publish future price and content changes without staging

Part 4 of the Catalog at Scale series — future prices need a publisher, not a reminder.

Commerce staging lets merchants schedule content and price changes into campaigns that publish themselves. Open Source has no staging module, but campaigns still exist: seasonal launches, embargoed products, price changes that take effect Monday at midnight. On Open Source at scale, cron becomes the publisher, with scheduled imports and queued catalog_product_entity updates doing the work staging would have done.

What staging actually gives you

Staging bundles three capabilities: a future state for entities, a timeline that activates it, and a preview of the future state. Reproducing all three on Open Source is overkill for most teams. What campaigns need is narrower: a price or content change that goes live at a known time, applied exactly once, with a rollback plan and an audit trail.

Design for that scope. A scheduled price change is a dated instruction: set SKUs A through F to price P starting at timestamp T. A scheduled content change is the same shape with different attributes: update names, descriptions, or category assignments at T. Both reduce to a job that runs at T, writes the values, and verifies the result. Everything else is process around that job.

The cron-driven publisher pattern

The reliable shape has three parts: a schedule source, an executor, and a verifier.

The schedule source is a table or a file the business owns. A simple scheduled_catalog_update table with entity type, SKU list or query reference, attribute payload as JSON, publish_at timestamp, status, and actor covers most cases. Merchants or the ERP insert rows with future timestamps; nothing executes early. A CSV drop with an effective-date column works the same way when the ERP already produces feeds.

The executor is a custom cron job, running every five to fifteen minutes, that selects due rows with status pending and publish_at in the past, locks them by setting status to running, applies the updates through repositories or the import API, then marks them done with a result reference. Locking matters: overlapping cron executions must never apply the same instruction twice. A status transition inside a transaction, or a running_at claim with a timeout, prevents double application when executions overlap.

The verifier runs after application: reload the affected products from catalog_product_entity and its value tables, confirm the new values, trigger the affected indexers, and record the outcome on the schedule row. Verification output is the audit trail staging would have provided.

Price scheduling on catalog_product_entity

Scheduled price changes write the price and special_price attributes with their date windows, or tier rows for quantity pricing. Two approaches work, and the choice depends on volume.

For tens to hundreds of SKUs, write through the product repository in batches inside the cron job. Batch by fifty to one hundred products, save, and log per batch so a failure resumes mid-list instead of restarting. For thousands of SKUs, stage a CSV and run it through the import subsystem with the same behavior settings every time, so the scheduled path and the manual path produce identical results. The import path also reuses existing validation and error reports instead of inventing new ones.

Special prices with special_from_date and special_to_date handle many campaigns without any custom scheduling: set the window in advance and the price index picks it up when it becomes active. The missing piece is process, not code. Somebody must enter the windows, and the price indexer must run after the window opens. Pair every future special price with a cron-driven reindex check so the new price actually surfaces at midnight instead of at the next manual reindex.

Catalog rule windows work the same way: create the rule in advance with from and to dates, disabled until the campaign, then enable it through the scheduled job at T. Never rely on an admin remembering to click enable at midnight.

Content scheduling without staging

Content changes, names, descriptions, category links, visibility, follow the same pattern with one extra concern: store-view scope. A launch that updates the default scope and three store views is four value writes per attribute, and partial application leaves views inconsistent. Apply all scopes for an entity in one transaction or one import row, and verify per scope afterwards.

Embargoed products use visibility and status plus category assignment timing. Create the product disabled and unassigned, then the scheduled job enables it and assigns categories at T. Search and category indexes must run after assignment, or the product exists but is unreachable from navigation. Sequence the job as update, reindex affected indexers, then verify storefront visibility, and alert when any step fails rather than silently retrying until the launch window passes.

CMS blocks and pages follow the same dated-instruction model with their own tables, applied by the same executor pattern. Keep one scheduler with per-entity appliers rather than a separate cron job per content type; a single due-list with typed handlers is easier to monitor than five independent schedules.

Safety: idempotency, rollback, and audit

Every scheduled instruction must be safe to retry. Repository saves keyed by SKU with full attribute payloads are naturally idempotent: applying twice yields the same values. Import rows behave the same way when the behavior is update rather than replace. Record the previous values on the schedule row before applying, so rollback is a new instruction with the old payload, not a database restore.

Guard the blast radius. Cap rows per execution, require an explicit product count on each instruction, and refuse instructions that match more products than declared. A typo in a category filter should fail closed, not reprice the catalog. Route instructions above a threshold, say 10k products, through a two-person approval recorded on the row before the executor will pick it up.

Monitor the scheduler like production infrastructure: alert on pending rows past their publish_at, on executions that stay running past their timeout, and on verification mismatches. A dashboard listing scheduled, running, done, and failed instructions with actor and timestamp answers most campaign postmortems before they start.

What you deliberately do not rebuild

Do not rebuild staging previews, versioned rollbacks across arbitrary entities, or visual timelines. Those are product features with real maintenance cost. The cron publisher covers the operational need, timed publication with audit, at a fraction of the complexity. If the business later moves to Commerce, each scheduled instruction maps cleanly onto a staging campaign, because the instruction already captures what changes, when, and for whom.

Scheduled publishing turns midnight launches from heroics into routine. The next post in this series moves from time to throughput: import pipelines that keep a large catalog fresh without choking the database.

Next in this series:

Continue with magento2-catalog-import-pipelines for import pipelines that keep a large catalog fresh at scale.

magento2catalogcontent