Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 03 · Revisions, publishing and approval

Scheduled publishing and the timezone

A record goes live when its published date passes and, for pages and books, comes down when its unpublish date passes. This page covers setting both, the worker that runs at each crossing, and the timezone question to settle before you rely on a schedule.

Who this is for. Developers and site builders who schedule content, or whose jobs and search results must follow a schedule. After this page you can schedule a record to go live and to expire, and say what happens, and when, at each crossing.

Scheduling a record

  • To go live later, set the record's published date in the future. Until then gate one hides it (see Published, active and draft: the two gates); from that moment the same condition lets it through. Nothing is saved when the date passes: going live is purely a matter of the clock.
  • To come down later, set unpublishAt. The admin has no field for it, so it is set through the API. It is honoured for pages, books, units and book pages only. Once the date has passed, the worker below unpublishes the record.

Through the API the two fields are published and unpublishAt on the record; Versioned content and the live revision explains both from the API's side.

The worker

One scheduled task, processScheduledPublishing, runs every minute on every site (cron * * * * *). It handles the records whose publish or unpublish date has been crossed since it last ran: it updates the search index, purges cached responses and raises the record's update event, which jobs listen to.

Going liveExpiring
TriggerThe published date has passedThe unpublish date has passed
What it writesNothing: the record is already visible by the clockUnpublishes the record and consumes its unpublish date
ThenRaises the update event (search index, jobs) and clears the response cacheThe same
Record typesBlogs, blog posts, books, units, book pages, FAQ questions, pages, product descriptionsBooks, units, book pages, pages
Older datesNot replayed (see below)Handled however old, in batches of 1,000; a record not yet handled is retried on the next run

A record becomes visible to a fresh query as soon as its published date passes, whether or not the worker has run. What waits for the worker is everything around the record: the search index, cached responses and jobs.

The worker keeps a mark of how far it has got and moves it forward before it handles each window, so a go-live crossing never fires its job twice. The mark starts at the current window when it is first set up, so a published date that was already in the past before then gets no go-live handling: no search update and no job. To bring the search index up to date after that, use "Reindex All" in the admin.

The timezone

A schedule set in one timezone and evaluated in another is the class of error worth ruling out first. Three facts settle it on Core dna:

  1. Dates are stored in UTC. Every date-time field, published and unpublishAt included, is converted to UTC before it is saved.
  2. Dates are read in the site's timezone. Every request (front end, admin and API) and every background worker runs in the timezone configured for the site (Globals.timezone). A date typed in the admin, or sent through the API, without an offset is taken as the site's local time and converted to UTC correctly. The editor's own computer clock plays no part.
  3. The gate compares against the database's clock. The condition published <= NOW() runs in the database.

So the author's timezone plays no part in when a record goes live.

An integration whose own clock runs in another zone should send dates with an explicit offset, such as 2026-11-01T09:00:00+11:00, so that nothing depends on the site's setting.

Comparing a date with now in a template

When a template shows or hides something by a date held in a layout area, compare timestamps, not formatted strings. A timestamp is the same instant in every zone; a formatted date carries the zone it was formatted in.

Template code
{$startDate = $component->getAreaDate('startDate')}
{if empty($startDate) or $startDate->getTimestamp() < $smarty.now}
  {* the start date is empty or has passed *}
{/if}
{% set startDate = component.getAreaDate('startDate') %}
{% if startDate is empty or startDate.timestamp < date().timestamp %}
  {# the start date is empty or has passed #}
{% endif %}
Go deeper

Was this page helpful?

Your answer goes to the docs team.