Block Bindings

Building a Custom AI Block Bindings Source

⏱ 19 min

The previous lesson’s reading-time source computed a value from data already on the site. This lesson builds a source that calls an AI model to produce the value: binding an image block’s alt attribute to an AI-generated description of that image. It’s a genuinely useful pattern, and also a good place to be honest about a real limitation of the Block Bindings API as it exists today, register_block_bindings_source() has no update_value_callback, so this source is inherently read-oriented.

What you'll learn in this lesson
An AI-backed get_value_callback
Reading the bound attribute's sibling data (the image's attachment ID) from $block_instance.
Why you must cache the AI result
get_value_callback runs at render time, potentially on every page view, not just once.
The real limitation: no write-back in registration
Why this pattern generates a value once and reads it after, rather than regenerating live.
Where a real "regenerate" action would live
A separate REST endpoint and editor button, not part of the binding source itself.
Prerequisites

The previous lesson’s register_block_bindings_source() walkthrough. An AI provider plugin and working API key (Course 5), since this source makes a real wp_ai_client_prompt() call.

Step 1: Register the source

Same registration shape as before, on init, with a namespaced name and a label:

// File: my-plugin/my-plugin.php
add_action( 'init', function () {
	register_block_bindings_source(
		'my-plugin/ai-image-alt',
		array(
			'label'              => __( 'AI Generated Alt Text', 'my-plugin' ),
			'get_value_callback' => 'my_plugin_get_ai_alt_text',
		)
	);
} );

This source doesn’t need uses_context, everything it needs (which attachment the image block points at) is already available on the block instance itself, not something it has to read from an ancestor block.

Step 2: Reading the attachment ID from the block instance

The image block’s own attributes, id and url, are available on $block_instance->attributes, the same object passed into get_value_callback. That means this source can resolve the attachment ID directly, without needing any args passed through the binding’s own markup:

// File: my-plugin/my-plugin.php
function my_plugin_get_ai_alt_text( $source_args, $block_instance, $attribute_name ) {
	if ( 'alt' !== $attribute_name ) {
		return null;
	}

	$attachment_id = $block_instance->attributes['id'] ?? 0;

	if ( ! $attachment_id ) {
		return null;
	}

	// ... generation and caching, Step 3.
}

Checking $attribute_name matters if you ever reuse one source function across more than one bound attribute, it’s the only argument that tells you which attribute is currently being resolved.

Step 3: Generating once, caching after

This is the step that matters most. get_value_callback runs every time the block renders, which on a published post means every page view, not once. Calling wp_ai_client_prompt() on every render would be slow, expensive, and pointless, an image’s alt description doesn’t change between requests. Cache the result as attachment meta and only call the model when nothing is cached yet:

// File: my-plugin/my-plugin.php
function my_plugin_get_ai_alt_text( $source_args, $block_instance, $attribute_name ) {
	if ( 'alt' !== $attribute_name ) {
		return null;
	}

	$attachment_id = $block_instance->attributes['id'] ?? 0;

	if ( ! $attachment_id ) {
		return null;
	}

	$cached = get_post_meta( $attachment_id, '_ai_alt_text', true );

	if ( $cached ) {
		return $cached;
	}

	$image_url = wp_get_attachment_image_url( $attachment_id, 'medium' );

	if ( ! $image_url ) {
		return null;
	}

	$result = wp_ai_client_prompt()
		->with_file( $image_url )
		->with_text( 'Write a concise, descriptive alt text for this image, under 125 characters. Do not start with phrases like "image of" or "picture of".' )
		->generate_text();

	if ( is_wp_error( $result ) ) {
		return null;
	}

	update_post_meta( $attachment_id, '_ai_alt_text', $result );

	return $result;
}

Bind the image block’s alt attribute to it the same way you tested a binding in the previous lesson:

<!-- wp:image {"id":42,"sizeSlug":"large","metadata":{"bindings":{"alt":{"source":"my-plugin/ai-image-alt"}}}} -->
<figure class="wp-block-image size-large"><img src="https://example.com/wp-content/uploads/photo.jpg" alt="" class="wp-image-42"/></figure>
<!-- /wp:image -->

Step 4: Being honest about the read-only shape

register_block_bindings_source() has exactly two arguments that matter for behavior, label and get_value_callback, plus the optional uses_context. There is no update_value_callback argument, nothing in the registration itself lets an editor write a new value back through this source. Once cached, the alt text this source returns stays whatever it was the first time it was generated, until something else, not the binding registration, clears or updates that cached meta value.

That “something else” has to be a separate mechanism you build yourself: a button in the editor that calls a REST endpoint which deletes the cached _ai_alt_text meta (forcing the next render to regenerate it), or a WP-CLI command an editor runs to regenerate alt text for a batch of images. Lesson 5 and 6 build exactly that kind of editor-triggered REST action, for a sidebar panel and a toolbar button respectively, the same pattern applies here if you want a “Regenerate alt text” control rather than relying purely on first-render generation.

Treat this as generate once, read many, not a live connection

Think of this source the way you’d think of a build step’s output, computed once, cached, and served after. If you need something closer to a live, editable connection, that has to be layered on top with your own REST endpoint and editor UI, the Block Bindings API by itself gives you the read side only.

Test it

Upload an image to the Media Library, insert an Image block referencing it, switch to Code Editor, and add the metadata.bindings markup from Step 3 with the correct attachment id. Switch back to the visual view. On first render, WordPress calls my_plugin_get_ai_alt_text(), which calls the AI model and writes _ai_alt_text to that attachment’s post meta:

wp-env run cli wp post meta get 42 _ai_alt_text

You should see the AI-generated description. Reload the post again, the value should come back instantly, confirming the cache is being read rather than a fresh AI call being made on every view.

A failed AI call caches nothing, and retries on every render

Because the function only caches on success, a failed request (a missing API key, a provider outage) leaves the meta empty and triggers another AI call on the very next render, and the one after that, potentially hammering the provider’s API on a high-traffic page with no cached value yet. For production use, consider also caching a short-lived “generation failed, don’t retry for N minutes” marker so a persistent failure doesn’t turn into a request storm.

Recap

This source calls wp_ai_client_prompt() inside get_value_callback, generating an image’s alt text from the attachment referenced in $block_instance->attributes['id'], and caches the result as post meta so the expensive AI call happens once, not on every page view. The Block Bindings API’s registration has no update_value_callback, so anything resembling “regenerate this value” has to be built as a separate REST endpoint and editor control, not as part of the source registration itself, exactly the kind of editor UI the next few lessons cover.

Resources & further reading

← Using the Block Bindings API Adding an AI Sidebar Panel →