The block you built in the previous lesson stores its own content directly as a block attribute. The Block Bindings API, shipped in WordPress 6.5, offers a different shape entirely: connecting a block attribute to a value that lives somewhere else and is computed at render time, post meta, a pattern override, or a source you register yourself. This lesson covers the API on its own terms, using a non-AI example, before the next lesson builds a source that returns an AI-generated value specifically.
A local WordPress 6.5 or newer environment. No specific block from the previous lesson is required, this lesson’s example uses a plain core paragraph block.
Step 1: What a binding source actually is
A block binding connects one attribute of one block instance to a value supplied by a
named source, instead of a value typed directly into that attribute. WordPress core
ships two built-in sources: core/post-meta, which binds to a post meta field, and
core/pattern-overrides, used for editable regions inside synced patterns. Registering
your own source with register_block_bindings_source() lets any block attribute pull
its value from wherever your plugin decides, computed fresh each time the block renders.
// File: my-plugin/my-plugin.php
add_action( 'init', function () {
register_block_bindings_source(
'my-plugin/reading-time',
array(
'label' => __( 'Estimated Reading Time', 'my-plugin' ),
'uses_context' => array( 'postId' ),
'get_value_callback' => 'my_plugin_get_reading_time',
)
);
} );
Sources are always registered on init, the same hook block types themselves use, not
on a separate hook the way abilities are. The source name follows the same
namespacing convention as everywhere else in WordPress: your-plugin-slug/source-name.
Step 2: get_value_callback, argument by argument
The callback receives up to three arguments: the source-specific $source_args from
the block’s own markup, the $block_instance (a WP_Block object, which is how
uses_context values become available), and the name of the attribute currently being
resolved:
// File: my-plugin/my-plugin.php
function my_plugin_get_reading_time( $source_args, $block_instance, $attribute_name ) {
$post_id = $block_instance->context['postId'] ?? null;
if ( ! $post_id ) {
return null;
}
$word_count = str_word_count( wp_strip_all_tags( get_post_field( 'post_content', $post_id ) ) );
$minutes = max( 1, (int) round( $word_count / 200 ) );
return sprintf(
/* translators: %d: number of minutes. */
_n( '%d minute read', '%d minute read', $minutes, 'my-plugin' ),
$minutes
);
}
$block_instance->context['postId'] only exists because uses_context declared
postId as something this source needs. Without listing it in uses_context, that key
simply won’t be present in $block_instance->context, this is the same context
mechanism ordinary blocks use to read values like the current post ID from an ancestor
block, not something specific to bindings.
Returning null is the correct way to say “no value available,” WordPress falls back
to the attribute’s default in that case rather than rendering a broken string.
Step 3: The markup shape a bound attribute produces
Once an attribute is bound, the block’s saved comment delimiter grows a
metadata.bindings entry describing the connection, instead of the attribute’s plain
value:
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"my-plugin/reading-time"}}}} -->
<p>1 minute read</p>
<!-- /wp:paragraph -->
The shape is always metadata.bindings.{attribute_name}.source, with an optional
sibling args key for sources that need extra parameters, exactly how core’s own
core/post-meta source passes which meta key to read:
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"subtitle"}}}}} -->
<p>Fallback text shown before the binding resolves</p>
<!-- /wp:paragraph -->
The plain text sitting inside the block markup itself is only a fallback, once the
binding resolves at render time, get_value_callback’s return value replaces it.
Test it
The fastest way to see a custom source resolve, without building any editor UI for it
yet, is the block editor’s Code Editor view. Add the PHP from Steps 1 and 2 to a plugin
file, open any post, switch to Code Editor (the ”⋮” options menu, or
Ctrl+Shift+Alt+M), paste in a paragraph block with the metadata.bindings
markup from Step 3, and switch back to the visual editor. The paragraph should display
the real, computed reading time text, not the fallback text you typed.
Once an attribute like content is connected to a binding source, WordPress locks it
against direct typing in the visual editor, you’ll see a small icon on the block
indicating it’s connected, and editing that field directly is disabled. This is
expected, the value is meant to come from the source, not from the author retyping it.
If you need an editable UI to attach and detach bindings rather than hand-editing
markup, that’s a separate piece of editor UI you’d build yourself, core provides the
attribute-locking behavior but not a polished connect/disconnect interface for every
custom source out of the box.
Recap
register_block_bindings_source() takes a namespaced source name, a required label
and get_value_callback, and an optional uses_context array for reading values like
the current post ID from the surrounding block tree. get_value_callback receives
$source_args, the $block_instance, and the attribute name, and its return value
replaces the block’s static content at render time. The markup shape,
metadata.bindings.{attribute}.source (plus optional args), is the same shape core’s
own core/post-meta and core/pattern-overrides sources use. The next lesson puts an
AI-generated value behind exactly this mechanism.
Resources & further reading
- Block Bindings API reference, developer.wordpress.org
- register_block_bindings_source() function reference, developer.wordpress.org
- Introducing Block Bindings, Part 2, developer.wordpress.org