TL;DR: post_submitbox_misc_actions still works exactly as documented — it just never fires on the editor most sites now use.
- Signature:
do_action( 'post_submitbox_misc_actions', $post ). Added in WordPress 2.9.0; the$postparameter arrived in 4.4.0. - It fires inside
post_submit_meta_box(), after the post date setting, inwp-admin/includes/meta-boxes.php. - The Publish box is registered with
__back_compat_meta_box => true, and the block editor skips every box carrying that flag — so the hook never runs there. - There is no error and no notice. Your callback simply never executes.
- The block editor equivalent is a
PluginPostStatusInfoslot registered in JavaScript.
/ Overview
When does post_submitbox_misc_actions fire?
When WordPress renders the classic Publish meta box, immediately after the post date row and before the box closes. It is the standard extension point for adding your own control — a checkbox, a select, a status line — into the panel that already holds Status, Visibility and Publish immediately.
The declaration in meta-boxes.php is short, and the version tags matter:
PHP — wp-admin/includes/meta-boxes.php
/** * Fires after the post time/date setting in the Publish meta box. * * @since 2.9.0 * @since 4.4.0 Added the $post parameter. * * @param WP_Post $post WP_Post object for the current post. */ do_action( 'post_submitbox_misc_actions', $post );
One parameter, a WP_Post. Code written before WordPress 4.4 calls get_post() inside the callback instead, which still works and is why so much of the sample code you will find online ignores the argument entirely.
/ The trap
Why does it do nothing in the block editor?
Because the box it lives in is never rendered there, and the mechanism that removes it is deliberate rather than accidental. WordPress registers the Publish box like this:
PHP — how submitdiv is registered
$publish_callback_args = array( '__back_compat_meta_box' => true ); add_meta_box( 'submitdiv', __( 'Publish' ), 'post_submit_meta_box', null, 'side', 'core', $publish_callback_args );
And do_meta_boxes() reads that flag when it walks the registered boxes:
PHP — wp-admin/includes/template.php
// If a meta box is just here for back compat, don't show it in the block editor. if ( $screen->is_block_editor() && isset( $box['args']['__back_compat_meta_box'] ) && $box['args']['__back_compat_meta_box'] ) { continue; }
That continue is the whole story. The box is skipped, so post_submit_meta_box() is never called, so do_action( 'post_submitbox_misc_actions', $post ) is never reached. Your add_action() registered successfully. The hook exists. It simply has no caller on that screen.
There is no deprecation notice, no admin warning and no PHP error. The only symptom is a control that used to be there and now is not — which is exactly the class of bug that survives a code review.
/ Family
What are the sibling publish-box hooks?
Four, and picking the right one saves a lot of CSS. They all live in the same file and all share the block-editor fate above.
| Hook | Since | Renders | Parameter |
|---|---|---|---|
| post_submitbox_start | 2.7.0 | Top of the publishing-actions row, next to Move to Trash | $post — or null on the Edit Link screen |
| post_submitbox_minor_actions | 4.4.0 | After Save Draft and Preview | $post |
| post_submitbox_misc_actions | 2.9.0 | After the date setting, inside the misc section | $post (since 4.4.0) |
| attachment_submitbox_misc_actions | 3.5.0 | The attachment edit screen’s Save box | $post |
Two details in that table catch people out. post_submitbox_start is documented as WP_Post|null because the Edit Link screen fires it with null — a callback that types the parameter as WP_Post or calls a method on it will fatal there. And attachments never reach post_submitbox_misc_actions at all: they render attachment_submit_meta_box() instead, which fires its own attachment_submitbox_misc_actions.
/ Rendering
How do you render a field in the Publish box?
Echo the markup, wrapped in the class WordPress already styles, and include a nonce. The misc-pub-section class gives you the same padding and separator as the built-in rows, so the control does not look bolted on:
PHP — a control in the Publish box
add_action( 'post_submitbox_misc_actions', function ( $post ) { if ( ! in_array( $post->post_type, [ 'post', 'product' ], true ) ) { return; } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return; } $notify = '1' === get_post_meta( $post->ID, '_yp_notify', true ); wp_nonce_field( 'yp_notify_save', 'yp_notify_nonce' ); ?> <div class="misc-pub-section yp-notify"> <label> <input type="checkbox" name="yp_notify" value="1" <?php checked( $notify ); ?> /> <?php esc_html_e( 'Notify downstream systems', 'your-plugin' ); ?> </label> </div> <?php } );
The capability check is not optional. The hook fires for anyone who can reach the edit screen, and contributors reach it for their own drafts.
/ Saving
How do you save what the field submits?
On save_post, guarded properly. The input is part of the normal post form, so it arrives in $_POST — but that same hook also runs for autosaves, revisions and REST requests that never included your field. Treating a missing key as "unchecked" without those guards will silently wipe the value every time an autosave fires.
PHP — the save handler
add_action( 'save_post', function ( $post_id ) { if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) { return; } if ( ! isset( $_POST['yp_notify_nonce'] ) ) { return; // not our form — leave the meta alone } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['yp_notify_nonce'] ) ), 'yp_notify_save' ) ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } update_post_meta( $post_id, '_yp_notify', empty( $_POST['yp_notify'] ) ? '0' : '1' ); } );
The isset( $_POST['yp_notify_nonce'] ) check is the important one, and it is the guard most snippets omit. It is what distinguishes "the user unchecked the box" from "this save did not come from the classic edit form at all".
/ Block editor
What is the block editor equivalent?
A PluginPostStatusInfo slot, registered in JavaScript against the same post meta. There is no PHP hook that renders into the block editor's Summary panel, because that panel is React and is never built server-side.
JS — the same control, block editor side
import { registerPlugin } from '@wordpress/plugins'; import { PluginPostStatusInfo } from '@wordpress/editor'; import { CheckboxControl } from '@wordpress/components'; import { useEntityProp } from '@wordpress/core-data'; const NotifyToggle = () => { const [ meta, setMeta ] = useEntityProp( 'postType', 'post', 'meta' ); return ( <PluginPostStatusInfo> <CheckboxControl label={ 'Notify downstream systems' } checked={ !! meta?._yp_notify } onChange={ ( value ) => setMeta( { ...meta, _yp_notify: value } ) } /> </PluginPostStatusInfo> ); }; registerPlugin( 'your-plugin-notify', { render: NotifyToggle } );
For useEntityProp to see the value, the meta key must be registered with show_in_rest — and a key beginning with an underscore is protected, so it also needs an auth_callback:
PHP — exposing the meta to both editors
register_post_meta( 'post', '_yp_notify', [ 'type' => 'boolean', 'single' => true, 'show_in_rest' => true, 'auth_callback' => function ( $allowed, $meta_key, $post_id ) { return current_user_can( 'edit_post', $post_id ); }, ] );
/ Both
How do you support both editors from one plugin?
Register both paths and let the screen decide, rather than trying to detect the editor and branch. The PHP hook is inert on block-editor screens and the JS plugin is never loaded on classic ones, so the two never collide.
- Register the meta once, with
show_in_restand anauth_callback, so both editors read and write the same key. - Keep the PHP hook for the classic editor, the Quick Edit path, and any post type that opted out of the block editor via
use_block_editor_for_post_type. - Enqueue the JS on
enqueue_block_editor_assetsso it only loads where the slot exists. - Do not duplicate the write. The block editor saves meta through the REST API, not through
save_post's$_POST— the nonce guard above already makes that a no-op, which is the point of it.
Check before you assume classic is gone. A post type registered with 'show_in_rest' => false still uses the classic editor, as does any site running the Classic Editor plugin. Dropping the PHP hook because "everything is Gutenberg now" removes the control for exactly the installs most likely to depend on it.
@since tags and the WP_Post|null signature of post_submitbox_start verified in wp-admin/includes/meta-boxes.php against WordPress 7.0.2.__back_compat_meta_box skip is in do_meta_boxes() in wp-admin/includes/template.php.auth_callback behaviour: register_post_meta().