Scale & Reach

Project: Analytics Reporter

⏱ 13 min

This project builds an ability that pulls real content statistics, post counts by category and by date, straight from WordPress, and hands those raw numbers to the AI Client SDK to produce a short, plain-language summary a non-technical stakeholder can read in ten seconds.

What you'll learn in this lesson
How to gather real content stats with WP_Query and wp_count_posts()
Post counts by status, by category, and across a date range.
How to hand raw numbers to wp_ai_client_prompt() for summarization
The model's job here is narration, not calculation.
Why the AI never computes the numbers itself
Keeping the actual counts exact and the AI's role limited to describing them.
How to keep summaries grounded in the specific numbers passed in
Avoiding vague, ungrounded commentary.
Prerequisites

Course 5 for wp_ai_client_prompt(). Enough published content with varied categories and dates that the resulting summary reflects something real rather than “1 post total.”

Step 1: Register the ability

Input is an optional date range. Output is both the raw stats (for anyone who wants the real numbers) and a plain-language summary.

// File: analytics-reporter/analytics-reporter.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'analytics-reporter/summarize-content',
		array(
			'label'               => __( 'Summarize content stats', 'analytics-reporter' ),
			'description'         => __( 'Pulls post counts by status, category, and date range, and returns both the raw numbers and a plain-language summary of publishing activity.', 'analytics-reporter' ),
			'category'            => 'analytics',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'since_days' => array( 'type' => 'integer' ),
				),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'stats'   => array( 'type' => 'object' ),
					'summary' => array( 'type' => 'string' ),
				),
			),
			'permission_callback' => function () {
				return current_user_can( 'edit_others_posts' );
			},
			'execute_callback'    => 'analytics_reporter_summarize',
			'meta' => array(
				'annotations' => array( 'readonly' => true ),
				'mcp'         => array( 'public' => true ),
			),
		)
	);
} );

Step 2: Gather real stats with wp_count_posts() and WP_Query

// File: analytics-reporter/analytics-reporter.php
function analytics_reporter_gather_stats( $since_days ) {
	$counts = wp_count_posts( 'post' );

	$since = date( 'Y-m-d', strtotime( "-{$since_days} days" ) );
	$recent_query = new WP_Query( array(
		'post_status'    => 'publish',
		'date_query'     => array( array( 'after' => $since ) ),
		'posts_per_page' => -1,
		'fields'         => 'ids',
	) );

	$categories = get_categories( array( 'hide_empty' => true ) );
	$by_category = array();
	foreach ( $categories as $category ) {
		$by_category[ $category->name ] = $category->count;
	}
	arsort( $by_category );

	return array(
		'published_total'    => (int) $counts->publish,
		'draft_total'        => (int) $counts->draft,
		'published_recent'   => $recent_query->found_posts,
		'recent_window_days' => $since_days,
		'top_categories'     => array_slice( $by_category, 0, 5, true ),
	);
}

wp_count_posts() returns an object with a property per post status, publish and draft counts come straight from it, no manual WP_Query counting required for those. The recent-activity and per-category numbers do need WP_Query and get_categories() respectively, since wp_count_posts() alone doesn’t break counts down by date or taxonomy.

Step 3: Hand the numbers to the AI Client SDK, and only the numbers

// File: analytics-reporter/analytics-reporter.php
function analytics_reporter_summarize( $input ) {
	$since_days = ! empty( $input['since_days'] ) ? (int) $input['since_days'] : 30;
	$stats      = analytics_reporter_gather_stats( $since_days );

	$categories_list = '';
	foreach ( $stats['top_categories'] as $name => $count ) {
		$categories_list .= "- {$name}: {$count} posts\n";
	}

	$prompt = <<<PROMPT
Write a short, plain-language summary (3-4 sentences) of this site's publishing
activity, for a non-technical stakeholder. Use only the numbers given below, do not
estimate or invent any figures not listed here.

Total published posts: {$stats['published_total']}
Draft posts waiting: {$stats['draft_total']}
Posts published in the last {$since_days} days: {$stats['published_recent']}
Top categories by post count:
{$categories_list}
PROMPT;

	$summary = wp_ai_client_prompt( $prompt )->generate_text();

	return array(
		'stats'   => $stats,
		'summary' => $summary ?: 'Summary unavailable, raw stats are still included above.',
	);
}

The instruction “use only the numbers given below, do not estimate or invent any figures” matters more than it might look. Models are fluent enough to produce a plausible-sounding sentence with a made-up number in it if the prompt leaves room for that, explicitly constraining the model to the supplied figures, and always returning the raw stats object alongside the prose summary, means anyone reading the output can verify the narration against the real numbers directly.

Test it

Run the ability and compare the summary against the raw stats it’s built from:

wp-env run cli wp eval '
$result = wp_get_ability( "analytics-reporter/summarize-content" )->execute( array( "since_days" => 30 ) );
print_r( $result );
'

Confirm every number mentioned in summary actually appears in stats, if the summary references a figure that isn’t in the raw data, that’s a sign the prompt needs tightening, not a fact you should trust.

Letting the model do the arithmetic instead of PHP

It’s tempting to ask the model something like “what percentage of posts are drafts,” and let it compute that in the response. Don’t, language models are not reliable calculators and can produce a plausible but wrong percentage. Compute any derived figures (percentages, averages, growth rates) in PHP before the prompt is built, and only ask the model to narrate numbers you’ve already calculated correctly.

Recap

This ability’s real content stats come entirely from wp_count_posts(), WP_Query, and get_categories(), deterministic WordPress functions with no AI involved. The wp_ai_client_prompt() call at the end has exactly one job: turning those already-correct numbers into readable prose, with the raw stats object always returned alongside the summary so the narration can be checked against the real data.

Resources & further reading

← Project: Multilingual Translation Workflow Project: Social and Newsletter Repurposing Tool →