wp_schedule_event didn’t work

Cron fires outside of the standard WordPress loop, so $post will not be filled with any post-related data when the function is called. It should be obvious, then, that providing an ID directly isn’t referencing the non-existent $post variable which is why it works in that scenario.

The best way to set post_statuses to expired is to change your function to something that loads posts which need to be expired and loops through them, using WP_Query:

function update_expired_field() {
    $today = getdate();
    $query = new WP_Query( 'meta_query' => array(
            'key'     => 'your_expire_meta_key',
            'value'   => $today['year'] . '-' . $today['month'] . '-' . $today['mday'],
            'type'    => 'DATE',
            'compare' => '<=',
        ),
        'fields' => 'ids',
    );

    foreach( $query->posts as $postid ) {
        wp_update_post( array(
            'ID'          => $postid,
            'post_status' => 'expired',
        ) );
    }
}

In the code I’ve shown querying the meta_key field from the database for a key called ‘your_expire_meta_key’, which needs to have values of the date you want the posts to expire in the format YYYY-MM-DD. We compare that date field against today’s date to determine which posts need expiring. The 'fields' => 'ids' tells WP_Query to only return post IDs so that we run as fast as possible. Finally we loop-through each post ID and update the relevant post to expired status.