Priority & Ordering

Priority decides which callbacks run first on a hook: lower numbers go earlier, 10 is the default, and ties break by registration order.

Syntaxadd_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 )

Every callback is registered at a priority, an integer defaulting to 10. When a hook fires, WP_Hook walks the priorities in ascending numeric order, so lower runs first. Negative numbers and values as large as PHP_INT_MAX are both legal; there is no fixed range.

How ties break

Callbacks sharing a priority run in the order they were registered. Each priority is an ordered array keyed by a unique id derived from the callback, which has a consequence worth knowing: registering the same callback at the same priority twice does not queue it twice, the second registration overwrites the first. Register it at a different priority and you get two entries that both run.

Ordering against other plugins

You cannot control load order — WordPress sorts the active_plugins option alphabetically by plugin file path when a plugin is activated. Order by priority instead. To run after a callback registered at 10, use 11; to run before it, use 9. Guessing large numbers like 9999 works until two plugins both guess it.

the_content in WordPress 6.8

PriorityCore callbacks
8WP_Embed::run_shortcode, WP_Embed::autoembed, block hooks
9do_blocks
10wptexturize, wpautop, shortcode_unautop, prepend_attachment
11do_shortcode
12wp_filter_content_tags
20convert_smilies

Example

Example · php
<?php
// In WordPress 6.8, core registers do_shortcode on the_content at priority 11.
// Where you sit relative to that number changes what your callback receives.

// Priority 9: raw post_content. Shortcodes are still [tags], paragraphs not yet added.
add_filter( 'the_content', 'acme_before_core', 9 );
function acme_before_core( $content ) {
	// Safe place to rewrite shortcode attributes before they are expanded.
	return str_replace( '[legacy_gallery', '[gallery', $content );
}

// Priority 20: after wpautop (10), do_shortcode (11) and wp_filter_content_tags (12).
// $content is now finished HTML.
add_filter( 'the_content', 'acme_after_core', 20 );
function acme_after_core( $content ) {
	if ( ! is_singular() || ! in_the_loop() || ! is_main_query() ) {
		return $content;
	}

	return $content . '<p>' . esc_html__( 'Last updated: ', 'acme' )
		. esc_html( get_the_modified_date() ) . '</p>';
}

When to use it

  • Append markup to post content after core has already run wpautop and do_shortcode.
  • Rewrite shortcode syntax before do_shortcode expands it at priority 11.
  • Make your query modification run last, after another plugin has had its turn.
  • Print a script tag before or after another plugin's output in wp_head.

More examples

Ties, duplicates, and registration order

Read this when a callback appears to run twice, or not at all, after a refactor. The overwrite rule also means you cannot deliberately run the same function twice at one priority — wrap it in a second named function if you genuinely need that.

Example · php
add_filter( 'the_title', 'acme_one' );        // priority 10, registered first
add_filter( 'the_title', 'acme_two' );        // priority 10, registered second
// Runs acme_one then acme_two.

add_filter( 'the_title', 'acme_one' );        // SAME callback, SAME priority (10)
// Still runs once. The internal key is 'acme_one', so this overwrote the first entry
// and acme_one keeps its original position in the priority-10 bucket.

add_filter( 'the_title', 'acme_one', 11 );    // different priority = a second entry
// Now acme_one runs at 10 AND again at 11.

// Negative priorities are legal and run before everything else.
add_action( 'init', 'acme_very_early', -100 );

// PHP_INT_MAX is the idiom for "absolutely last", but two plugins can both use it —
// in which case registration order (that is, alphabetical plugin folder order) decides.
add_filter( 'the_content', 'acme_absolutely_last', PHP_INT_MAX );

Running after another plugin, reliably

The correct way to sequence against a third-party plugin whose priority you cannot assume. wp_loaded is the earliest point at which everyone has finished registering. Note the === false test: has_filter() can legitimately return 0, which is falsy but means priority zero.

Example · php
// Another plugin does: add_filter( 'the_content', 'other_plugin_content' );
// Its priority is the default 10, and it loads before or after you depending on
// alphabetical plugin folder order — which you must not depend on.

add_action( 'wp_loaded', 'acme_position_filters' );
function acme_position_filters() {
	// By wp_loaded, every plugin and the theme have registered their hooks.
	// has_filter() returns the priority as an int, or false.
	$their_priority = has_filter( 'the_content', 'other_plugin_content' );

	if ( false === $their_priority ) {
		add_filter( 'the_content', 'acme_content', 20 ); // sensible default
		return;
	}

	// Sit one step after whatever they chose.
	add_filter( 'the_content', 'acme_content', $their_priority + 1 );
}

Discussion

  • Be the first to comment on this lesson.