Real Usage

Adding AI Features to Your Own Plugin

⏱ 18 min

Everything so far has been individual pieces: setup, text generation, embeddings, error handling, provider preference. This lesson wires all of it into one complete, realistic feature: an admin meta box on the post edit screen with a “Generate Summary” button that calls out to whichever AI provider is configured, shows the result, and lets an editor save it as the post excerpt. It’s a small feature, but it touches every seam a real plugin has to handle, capability checks, nonces, AJAX, and the error states from Lesson 5, not just the AI call itself.

What you'll learn in this lesson
Registering an admin meta box
add_meta_box(), scoped to the post edit screen.
Wiring a button to an AJAX handler
Nonce verification and a capability check before anything calls out to an AI provider.
Calling the AI Client SDK from that handler
Using the builder chain and error handling from earlier lessons, not a bare, unchecked call.
Returning a usable result to the browser
A JSON response the meta box's JavaScript can display or fail on cleanly.
Prerequisites

Lessons 1 through 6. This lesson assumes you can already write the individual wp_ai_client_prompt() calls, error checks, and preference lists it combines, and focuses on the plugin architecture around them.

Step 1: Register the meta box

// File: my-plugin/class-summary-meta-box.php
add_action( 'add_meta_boxes', function () {
	add_meta_box(
		'my-plugin-ai-summary',
		__( 'AI Summary', 'my-plugin' ),
		'my_plugin_render_summary_meta_box',
		'post',
		'side'
	);
} );

function my_plugin_render_summary_meta_box( $post ) {
	wp_nonce_field( 'my_plugin_generate_summary', 'my_plugin_summary_nonce' );
	?>
	<p>
		<button type="button" class="button" id="my-plugin-generate-summary" data-post-id="<?php echo esc_attr( $post->ID ); ?>">
			<?php esc_html_e( 'Generate Summary', 'my-plugin' ); ?>
		</button>
	</p>
	<div id="my-plugin-summary-result"></div>
	<?php
}

The nonce field is rendered here, in the meta box itself, and read back on the JavaScript side when the AJAX request fires. This is the same pattern any other WordPress admin AJAX feature uses, nothing about talking to an AI provider changes how you’d protect this endpoint.

Step 2: The button’s JavaScript

// File: my-plugin/class-summary-meta-box.php
add_action( 'admin_enqueue_scripts', function ( $hook ) {
	if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
		return;
	}

	wp_add_inline_script( 'jquery', <<<JS
		jQuery( function ( $ ) {
			$( '#my-plugin-generate-summary' ).on( 'click', function () {
				var button   = $( this );
				var postId   = button.data( 'post-id' );
				var nonce    = $( '#my_plugin_summary_nonce' ).val();
				var resultEl = $( '#my-plugin-summary-result' );

				button.prop( 'disabled', true );
				resultEl.text( 'Generating...' );

				$.post( ajaxurl, {
					action: 'my_plugin_generate_summary',
					post_id: postId,
					nonce: nonce
				} ).done( function ( response ) {
					if ( response.success ) {
						resultEl.text( response.data.summary );
					} else {
						resultEl.text( 'Error: ' + response.data.message );
					}
				} ).fail( function () {
					resultEl.text( 'Request failed. Try again.' );
				} ).always( function () {
					button.prop( 'disabled', false );
				} );
			} );
		} );
	JS );
} );

Step 3: The AJAX handler, capability check and nonce first

The handler is where the actual AI call happens, but the capability check and nonce verification come before it, exactly as they would for any other admin-ajax action that changes or reads privileged data:

// File: my-plugin/class-summary-meta-box.php
add_action( 'wp_ajax_my_plugin_generate_summary', function () {
	check_ajax_referer( 'my_plugin_generate_summary', 'nonce' );

	$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;

	if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
		wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
	}

	$post = get_post( $post_id );

	if ( ! $post || '' === trim( $post->post_content ) ) {
		wp_send_json_error( array( 'message' => 'This post has no content to summarize.' ) );
	}

	$summary = my_plugin_generate_summary( $post->post_content );

	if ( is_wp_error( $summary ) ) {
		wp_send_json_error( array( 'message' => $summary->get_error_message() ) );
	}

	wp_send_json_success( array( 'summary' => $summary ) );
} );

check_ajax_referer() fails the request outright if the nonce doesn’t match, and the current_user_can( 'edit_post', $post_id ) check confirms the current user is actually allowed to edit this specific post, not just that they’re logged in. Both checks run before anything touches the AI Client SDK, so a request that shouldn’t happen never reaches the point of costing you an API call at all.

Step 4: The actual AI call, with the error handling from Lesson 5

// File: my-plugin/class-summary-meta-box.php
function my_plugin_generate_summary( string $content ) {
	try {
		$result = wp_ai_client_prompt()
			->with_text( wp_strip_all_tags( $content ) )
			->using_system_instruction(
				'Summarize the following WordPress post content in two sentences, ' .
				'plain language, suitable for use as a post excerpt.'
			)
			->using_temperature( 0.3 )
			->using_max_tokens( 150 )
			->using_model_preference( 'claude-sonnet-4-5', 'gemini-3-pro-preview', 'gpt-5.1' )
			->generate_text();

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

		return $result;
	} catch ( Throwable $e ) {
		return new WP_Error( 'ai_summary_failed', $e->getMessage() );
	}
}

Every piece here is something an earlier lesson already covered individually: with_text() and using_system_instruction() from Lesson 3, using_model_preference() from Lesson 6, and the try/catch plus is_wp_error() combination from Lesson 5. Nothing about combining them into one real feature required new SDK surface, this is the actual, unglamorous shape production AI-powered plugin code takes.

Don't skip wp_strip_all_tags() on post content

Passing raw post content, including any HTML markup, straight into a prompt wastes tokens on markup the model doesn’t need to produce a good summary, and can occasionally confuse a model into commenting on the markup itself rather than the content. Strip tags before building the prompt, and check that a stripped post still has meaningful content before calling out to a provider at all, this is what the “no content to summarize” check in Step 3 is doing.

Step 5: Letting the editor save the result

The last piece is making the generated summary actually useful, not just displayed. Extend the JavaScript’s success handler to offer saving it as the post excerpt via the same REST API the block editor already uses, rather than inventing a separate save mechanism:

// File: my-plugin/class-summary-meta-box.php - excerpt of the success handler
resultEl.html(
	'<p>' + response.data.summary + '</p>' +
	'<button type="button" class="button button-primary" id="my-plugin-save-excerpt">Use as Excerpt</button>'
);

$( document ).on( 'click', '#my-plugin-save-excerpt', function () {
	wp.data.dispatch( 'core/editor' ).editPost( { excerpt: response.data.summary } );
} );

Using wp.data.dispatch( 'core/editor' ).editPost() updates the excerpt field the same way any other block editor interaction would, the editor still reviews and explicitly saves the post afterward, the AI-generated text never gets written to the database without a human in the loop clicking save.

Test it: generate a summary from the command line first

Before wiring up the UI, confirm the underlying function works in isolation:

wp-env run cli wp eval '
$post = get_post( 1 );
$summary = my_plugin_generate_summary( $post->post_content );
var_dump( is_wp_error( $summary ) ? $summary->get_error_message() : $summary );
'

You should see a real, two-sentence summary string, not a WP_Error message. Then confirm the meta box itself, on the post edit screen, clicking “Generate Summary” should show the same kind of result within a few seconds.

Recap

A real AI-powered feature is mostly ordinary WordPress plugin code: a meta box, a nonce, a capability check, an AJAX handler, wrapped around one call into the AI Client SDK built from pieces this course already covered individually, system instructions, temperature, model preference, and WP_Error/try-catch handling. The AI call is a small part of the total code, and it should be treated with the same scrutiny as any other network call a plugin makes, never trusted blindly, never allowed to write directly to the database without review.

Resources & further reading

← Provider-Switching for Cost and Quality Combining the AI Client SDK with the MCP Adapter →