- Adding an expiry date to posts
- About the PublishPress Future cron jobs
- This applies to both features that schedule things for the future:
- How the scheduling actually works
- What happens when the cron job runs
- Who triggers the queue: WordPress vs. your server
- Why the default mode is a problem for expiration dates
- Setting up a reliable cron trigger
- How often should the cron run?
- Verifying it's working
In another documentation article, we explained that the scheduling is handled by a cron job and the table._ppfuture_actions_args
This post will explain more about that process and how it works.
Adding an expiry date to posts #
On every post save, the PublishPress Future plugin will check if a Future action is enabled in the post.
- If the feature is enabled, the plugin stores the data set in the Future Action metabox in the _ppfuture_actions_args table and schedules a cron task.
- If the feature is not enabled, it will ignore any cron task for it.
About the PublishPress Future cron jobs #
This applies to both features that schedule things for the future: #
- Future Actions — a single scheduled change to a post (expire, trash, change status, etc.)
- Action Workflows → Scheduled Action / Schedule delay step — any workflow step that runs later instead of immediately
Both are handled by the same underlying job queue, so everything below applies equally to a post expiration and to a delayed workflow step.
How the scheduling actually works #
Since PublishPress Future 3.0, scheduled jobs are not plain WordPress cron events. They’re queued in Action Scheduler — the same background-job library WooCommerce uses for orders, subscriptions, and webhooks. Each scheduled item is stored as a database row (in wp_actionscheduler_actions) with a hook name, arguments, and a due date/time, rather than living only in WordPress’s cron option like a classic wp_schedule_event() call.
Why this matters for you: Action Scheduler is more reliable than plain WP-Cron (survives PHP crashes, gives you a visible admin log at Future Actions / Action Workflows → Scheduled Actions, retries, batches). But it still needs something to trigger it to check the queue — and by default, that something is still WP-Cron underneath.
What happens when the cron job runs #
When the scheduled task runs, it calls the publishpressfuture_run_workflow action, passing the post ID stored as the task’s argument.
The task then reads the ppfuture_actions_args table for that post to look up the rest of the settings for the scheduled change — such as which action type was selected (expire, trash, change status, etc.).
Finally, it carries out the future action according to what was configured for that post.
Note: this supersedes the older
publishpressfuture_expirehook, which is deprecated. If you’re hooking into this programmatically or writing--hooks=filters for debugging, usepublishpressfuture_run_workflow.
Who triggers the queue: WordPress vs. your server #
WordPress does not have a real, always-on cron daemon by default. It uses “WP-Cron,” triggered opportunistically, controlled by the DISABLE_WP_CRON constant in wp-config.php. Action Scheduler hooks into this same system (via the action_scheduler_run_schedule cron event) unless you trigger it another way:
| Constant value | What happens |
|---|---|
Not defined, or define('DISABLE_WP_CRON', false); | Default WordPress behavior. WP-Cron runs on page/visit load. On every front-end request, WordPress checks if any scheduled events are due and runs them inline before serving the page. |
define('DISABLE_WP_CRON', true); | WordPress’s own trigger is disabled. Nothing runs cron automatically anymore. Something else — a real server cron job — must trigger it. |
Why the default mode is a problem for expiration dates #
With the default (DISABLE_WP_CRON not set), cron only fires when a visitor loads a page:
- Low-traffic sites: if a post is scheduled to expire at 2:00 AM and nobody visits the site until 9:00 AM, the post doesn’t expire until 9:00 AM. The scheduled time is a minimum, not a guarantee.
- High-traffic sites: WP-Cron runs on nearly every page load, which adds unnecessary overhead — checking the cron table dozens of times a minute when once every few minutes would do.
Either way, relying on visit-triggered cron is imprecise. For time-sensitive actions — post expiration or a workflow’s scheduled step — we recommend disabling WordPress’s built-in trigger and using a real, server-level cron job instead.
Setting up a reliable cron trigger #
Step 1: Disable WordPress’s own trigger #
In wp-config.php, add:
define('DISABLE_WP_CRON', true);
Place this above the line /* That's all, stop editing! */.
Step 2: Add a real cron job on the server #
Once WordPress’s trigger is off, you must add a system-level cron job, or scheduled events will never run. Choose one of the methods below.
Option A — curl hitting wp-cron.php (simplest, no shell access to WP-CLI needed) #
*/5 * * * * curl -s --max-time 30 "https://example.com/wp-cron.php?doing_wp_cron" > /dev/null 2>&1
- Runs every 5 minutes.
-ssilences output,--max-time 30prevents a hung request from piling up.- Discards output; add logging (
>> /var/log/wp-cron.log 2>&1) if you want a trail.
wget equivalent, if curl isn’t available:
*/5 * * * * wget -q -O /dev/null "https://example.com/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
Option B — WP-CLI (recommended if you have shell access) #
Two WP-CLI commands both work here — use whichever fits your setup:
bash
# Most direct: runs the Action Scheduler queue synchronously, right now.
*/5 * * * * cd /path/to/wordpress && wp action-scheduler run --quiet
# Also works: fires WordPress's own due cron events, which includes the
# action_scheduler_run_schedule event that kicks off the same queue.
*/5 * * * * cd /path/to/wordpress && wp cron event run --due-now --quiet
Why WP-CLI is generally the better option than curl/wget:
- Runs in the WordPress bootstrap directly — no HTTP round trip, no dependency on the site being publicly reachable at
wp-cron.php. - Doesn’t expose
wp-cron.phpto the outside world at all, which removes a low-value but real attack surface (some sites get hit with automated DoS-style floods of repeatedwp-cron.phprequests). wp action-scheduler runskips the WP-Cron event layer entirely and drains the job queue directly — useful if you want scheduled posts/workflow steps processed without waiting on WordPress’s own cron event timing.- You can scope it to just this plugin’s jobs while debugging, e.g.
wp action-scheduler run --hooks=publishpressfuture_run_workflow. --quietsuppresses output for clean cron logs; drop it while testing.
If wp isn’t in your PATH inside cron’s minimal environment, use the full path:
*/5 * * * * cd /path/to/wordpress && /usr/local/bin/wp action-scheduler run --quiet
Option C — Third-party uptime/cron pinger #
If you don’t have server/crontab access (common on some managed hosts), a service like cron-job.org, EasyCron, or your host’s built-in “cron jobs” panel can hit the same URL as Option A on a schedule. Functionally identical to Option A, just externally triggered instead of from the server itself.
How often should the cron run? #
Every 5 minutes is a reasonable default — it means a scheduled action (expiration or workflow step) fires within 5 minutes of its due time, worst case. Tighten to every 1 minute for time-critical use cases; there’s no real cost to running these commands that often since they’re a no-op when nothing is due.
Verifying it’s working #
In WordPress admin: go to Future Actions or Action Workflows → Scheduled Actions. Confirm the item shows status “Scheduled” with the correct arguments and scheduled date. After it runs, it should flip to “Complete” — click “View log” for a full execution trace.
From the command line, list what Action Scheduler has pending:
bash
wp action-scheduler list --status=pending
Site Health check: WordPress admin → Tools → Site Health → Passed tests → look for “Scheduled events are running.” If you see a recommended improvement saying “A scheduled event is late,” check whether it’s referencing action_scheduler_run_schedule — if so, that’s your trigger not firing on time, which points back to the crontab entry above.
After the scheduled time passes, re-check — pending items should move to complete, and the post/workflow step should reflect the expected change.
