save_post fires for autosaves, revisions, quick edit, bulk edit and XML-RPC as well as for someone pressing Update. None of those requests contain the fields from your meta box, so a handler that reads $_POST and writes what it finds does not fail — it succeeds, writing an empty string over a value that was correct thirty seconds ago.
add_action( 'save_post_datasheet', function ( $post_id, $post ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) || 'auto-draft' === $post->post_status ) {
return;
}
if ( ! isset( $_POST['datasheet_nonce'] )
|| ! wp_verify_nonce( $_POST['datasheet_nonce'], 'save_datasheet' ) ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
update_post_meta( $post_id, '_part_number', sanitize_text_field( $_POST['part_number'] ) );
}, 10, 2 );
The nonce check is doing more work here than its name suggests. It is there for CSRF, and it also happens to be the only reliable signal that this request came from your meta box at all — an autosave, a quick edit and a bulk edit each carry a nonce of their own and never yours. Checking isset() on the field instead does not work, because an unchecked checkbox is absent from the request body exactly like an autosave. The capability check is separate and not optional: save_post is reached by any user who can reach the editor, and edit_post against the specific id is the question worth asking. save_post_{$post_type} saves a string comparison at the top and is available from 3.7.