Automatically delete posts one by one depending on published time

Let me answer your questions starting from the second one… So…

Why is it deleting all posts at once?

You scheduled you event correctly, so it will run once a day. And in the function that is run, you select some posts and delete them, if they’re expired:

$args = array(
    'post_type' => 'post',
    'category_name' => 'stories',
    'posts_per_page' => -1
);

$stories = new WP_Query($args);

The code above will select all posts (because posts_per_page = -1) from “stories” category.

And here you loop through all of these posts, check if given post has expired and delete it if so:

    while($stories->have_posts()): $stories->the_post();    

        $expiration_date = get_post_meta( get_the_ID(), 'expiry_story_date', true );
        $expiration_date_time = strtotime($expiration_date);

        if ($expiration_date_time < time()) {
            wp_delete_post(get_the_ID(),true);                 
        }

    endwhile;

That’s why it deletes all posts at once.

So how to make it delete only one post?

The easiest way to modify your code is to add one line that will stop the loop after first post gets deleted:

        if ($expiration_date_time < time()) {
            wp_delete_post(get_the_ID(),true);    
            return;             
        }

But of course this is not the nicest way to do this.

Much better approach would be to get only the posts that should be deleted and get only one of them:

$args = array(
    'post_type' => 'post',
    'category_name' => 'stories',
    'posts_per_page' => 1,
    'meta_query' => array(
        array( 'key' => 'expiry_story_date', 'compare' => '<=', 'value' => date('Y-m-d') ) // here’s the very important part - you have to change the date format so it’s the same as the format you store in your custom fields
    )
);

$expired_stories = new WP_Query($args);