I use the below code to delete custom posts with status 'expired' (thanks to Jamie Keefer). Posts are set as 'expired' by a 3rd party plugin. Users have only a frontend access to their posts (adverts).

My question is: how to delete them after a number of days after they expired if post authors don't republish them? Also, I will appreciate any suggestions about how to improve this code.

// expired_post_delete hook fires when the Cron is executed
add_action( 'expired_post_delete', 'delete_expired_posts' );

// This function will run once the 'expired_post_delete' is called
function delete_expired_posts() {

    $todays_date = current_time('mysql');

    $args = array(
        'post_type' => 'advert',
        'post_status' => 'expired',
        'posts_per_page' => -1

    $posts = new WP_Query( $args );

    // The Loop
    if ( $posts->have_posts() ) {

        while ( $posts->have_posts() ) {

    } else {
            // no posts found

    /* Restore original Post Data */

// Add function to register event to WordPress init
add_action( 'init', 'register_daily_post_delete_event');

// Function which will register the event
function register_daily_post_delete_event() {
    // Make sure this event hasn't been scheduled
    if( !wp_next_scheduled( 'expired_post_delete' ) ) {
        // Schedule the event
        wp_schedule_event( time(), 'daily', 'expired_post_delete' );


This is how posts are set as 'expired':

add_action( 'adverts_event_expire_ads', 'adverts_event_expire_ads' );

 * Expires ads
 * Function finds Adverts that already expired (value in _expiration_date
 * meta field is lower then current timestamp) and changes their status to 'expired'.
 * @since 0.1
 * @return void
function adverts_event_expire_ads() {

    // find adverts with status 'publish' which exceeded expiration date
    // (_expiration_date is a timestamp)
    $posts = new WP_Query( array( 
        "post_type" => "advert",
        "post_status" => "publish",
        "meta_query" => array(
                "key" => "_expiration_date",
                "value" => current_time( 'timestamp' ),
                "compare" => "<="
    ) );

    if( $posts->post_count ) {
        foreach($posts->posts as $post) {
            // change post status to expired.
            $update = wp_update_post( array( 
                "ID" => $post->ID,
                "post_status" => "expired"
            ) );
        } // endforeach
    } // endif

  • Don' your code work or are you just looking to do it better. There are definite bugs in your code and it is also really expensive. There are plenty room for improvement here Commented Nov 18, 2015 at 9:44
  • @pieter-goosen It seems to work. I tested it once, one expired post was deleted, but I am not sure if this was my code, as an other plugin with a similar function was active at that moment. I deactivated that plugin (I don't like it, too big) and now I am waiting to see if the second one will be deleted (changed the wp_schedule_event() to hourly). And sure, I want also to improve this code, but I am not the recommended expert for this type of work :).
    – Yuri
    Commented Nov 18, 2015 at 10:52
  • @pieter-goosen Checked today, the second expired post was not deleted :(.
    – Yuri
    Commented Nov 19, 2015 at 7:33
  • 1
    wp-cron jobs aren't very reliable, someone needs to be logged in for a wp-cron job to execute. It does not work uatomatically like server crons. I see that the answer from @birgire did not solve your issue. You should file an edit and explain how the expired post status are set, Commented Nov 19, 2015 at 8:26
  • 2
    Note, the function that sets the post status is also very expensive. Commented Nov 19, 2015 at 10:20

2 Answers 2


WP Cron jobs are not reliable as it needs someone to visit the site at the time the event should fire. If you need precise timing, you should use server cron jobs.

Anyways, lets look at your code and what is wrong and we can fix it

  • wp is a better hook to use to hook your scheduled event, this is the earliest that postdata available. init is way to early. I probably think for safety, you can also try the template_redirect hook.

  • Your code are very very expensive to run which unnecessarily wastes server resources. We need to look at the following:

    • We do not need any postdata except the post ID. Any other postdata are useless, and querying it waste a lot of resources. On large sites, this can actually lead to fatal errors due to timing out

    • We only need to query posts which has expired and the expiry date has reached a certain timeframe.

Lets put everything in code: (NOTE: This all untested, I have also copied and pasted some of your code, so I might have missed something, and also, the code requires PHP 5.4+)

function get_exired_posts_to_delete()
     * If you need posts that expired more than a week ago, we would need to
     * get the unix time stamp of the day a week ago. You can adjust the relative 
     * date and time formats as needed. 
     * @see http://php.net/manual/en/function.strtotime.php
     * @see http://php.net/manual/en/datetime.formats.php
    // As example, we need to get posts that has expired more than 7days ago
    $past = strtotime( "- 1 week" );

    // Set our query arguments
    $args = [
        'fields'         => 'ids', // Only get post ID's to improve performance
        'post_type'      => 'advert',
        'post_status'    => 'expired',
        'posts_per_page' => -1,
        'meta_query'     => [
                'key'     => '_expiration_date',
                'value'   => $past,
                'compare' => '<='
    $q = get_posts( $args );

    // Check if we have posts to delete, if not, return false
    if ( !$q )
        return false;

    // OK, we have posts to delete, lets delete them
    foreach ( $q as $id )
        wp_delete_post( $id );

Now we can create our custom hook to hook our function

// expired_post_delete hook fires when the Cron is executed
add_action( 'expired_post_delete', 'get_exired_posts_to_delete' );

Lastly, schedule our event

// Add function to register event to wp
add_action( 'wp', 'register_daily_post_delete_event');
function register_daily_post_delete_event() {
    // Make sure this event hasn't been scheduled
    if( !wp_next_scheduled( 'expired_post_delete' ) ) {
        // Schedule the event
        wp_schedule_event( time(), 'daily', 'expired_post_delete' );

This should about do it, just remember to set the correct time frame inside the function

  • Pieter, thank you very much! I teste the code and it works very well. I only changed the wp_delete_post( $id ); to wp_trash_post( $id ); because the first deletes expired posts permanently and the second just trash them, so post authors will have a last chance to restore an important post (with admin help). Thanks again! P.S. I will try to improve myself the expensive function that sets the post status.
    – Yuri
    Commented Nov 19, 2015 at 17:10
  • Glad it worked as expected. As always, my code can be extented and modified to suite your exact needs. Enjoy ;-) Commented Nov 19, 2015 at 17:19
  • Pieter, I do not understand why the 'fields' => 'ids' is needed, without it the code will not work? The other arguments are not enough?
    – Yuri
    Commented Nov 19, 2015 at 18:20
  • This is quite an extensive question which will be better if asked as a new question. What do you men by The other arguments re not enough Commented Nov 19, 2015 at 18:31
  • 2
    'fields' => 'ids' are there for performance. We really only need the post ID's from the query as our delete/trash function only requires a post ID. When set, only an array of post ID's are returned, and not an array of post objects as per default. Setting fields to ids makes your query hundreds of times faster. You can still add any argument you desire to your query arguments, everything still works the same, exept the loop. But as I say, to completely answer this, you need to ask a new question because a proper explanation cnnot be done in comments ;-) Commented Nov 19, 2015 at 19:24

The native trash bin

It sounds like you're implementing your own version of the WordPress trash system.

If you trash a post, the native way, it will get the trash post status and will be automatically deleted (permanently) after 30 days. You can change that in your wp-config.php file with:

define( 'EMPTY_TRASH_DAYS', 10 );

for e.g. 10 days.

You can then trash your post with wp_trash_post( $post_id ) instead of creating your own custom expire post status.

This way you let the system work with you ;-)

For more info check the trash status in the Codex.

  • Posts (CPT 'adverts') are marked as 'expired' by a plugin, I just want to delete them a few days after 'expiring' if post authors don't republish them. I can't trash these posts because post authors have access only to their published or expired posts on the frontend, not to the backend (and to trashed posts).
    – Yuri
    Commented Nov 19, 2015 at 7:24
  • 1
    Your question should contain this new information (3rd party plugin, frontend access only, republish) so please update it. ps: It should be possible via filter to override the post status from expire to trash.
    – birgire
    Commented Nov 19, 2015 at 10:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.