Editorial Automation

Project: Content Moderation Tool

⏱ 14 min

This project builds an ability that classifies a comment (or any submitted content) for spam and policy violations using the AI Client SDK. It returns a recommendation with a reason, it does not delete, trash, or approve anything on its own.

What you'll learn in this lesson
How to structure a classification prompt with fixed categories
Getting a consistent, parseable answer instead of free-form prose.
How to return a recommendation instead of taking action
Keeping moderation decisions with a human reviewer.
How to log AI moderation recommendations for review
Using comment meta so a moderator can see the reasoning later.
Where automatic action is and isn't appropriate
The line between flagging and deleting, and why this project stays on the flagging side.
Prerequisites

Course 5 for wp_ai_client_prompt(). A few test comments, including at least one that reads as obvious spam, to confirm the classifier actually distinguishes cases.

Step 1: Register the ability

Input is a comment ID. Output is a classification, a confidence-style label, and a short reason, deliberately not a boolean “delete this” flag.

// File: content-moderator/content-moderator.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'content-moderator/classify-comment',
		array(
			'label'               => __( 'Classify comment for moderation', 'content-moderator' ),
			'description'         => __( 'Classifies a comment as likely spam, policy violation, or acceptable, with a reason. Returns a recommendation only, never deletes or approves anything.', 'content-moderator' ),
			'category'            => 'moderation',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array( 'comment_id' => array( 'type' => 'integer' ) ),
				'required'   => array( 'comment_id' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'classification' => array( 'type' => 'string', 'enum' => array( 'acceptable', 'likely_spam', 'policy_violation' ) ),
					'reason'          => array( 'type' => 'string' ),
				),
			),
			'permission_callback' => function () {
				return current_user_can( 'moderate_comments' );
			},
			'execute_callback'    => 'content_moderator_classify',
			'meta' => array(
				'annotations' => array( 'readonly' => true ),
				'mcp'         => array( 'public' => true ),
			),
		)
	);
} );

readonly here is accurate in the strictest sense (this call itself changes nothing), even though it logs its result to comment meta as a side effect, that logging is informational, not a moderation action.

Step 2: Classify with the AI Client SDK and log the recommendation

// File: content-moderator/content-moderator.php
function content_moderator_classify( $input ) {
	$comment = get_comment( $input['comment_id'] );
	if ( ! $comment ) {
		return new WP_Error( 'not_found', 'No comment found with that ID.' );
	}

	$prompt = <<<PROMPT
Classify this comment into exactly one category: "acceptable", "likely_spam", or "policy_violation".
Policy violations include harassment, hate speech, and threats. Spam includes promotional links, repeated
gibberish, or content unrelated to the post. Respond in exactly this format:

Classification: <category>
Reason: <one sentence>

Comment author: {$comment->comment_author}
Comment content: {$comment->comment_content}
PROMPT;

	$response = wp_ai_client_prompt( $prompt )
		->using_model_preference( 'anthropic/claude', 'openai/gpt' )
		->generate_text();

	if ( empty( $response ) ) {
		return new WP_Error( 'empty_response', 'The AI provider returned no classification. Defaulting to manual review required.' );
	}

	$result = content_moderator_parse_response( $response );

	// Log the recommendation for a human moderator, don't act on it here.
	update_comment_meta( $comment->comment_ID, '_ai_moderation_classification', $result['classification'] );
	update_comment_meta( $comment->comment_ID, '_ai_moderation_reason', $result['reason'] );

	return $result;
}

function content_moderator_parse_response( $response ) {
	$classification = 'acceptable';
	$reason         = '';

	if ( preg_match( '/Classification:\s*(\w+)/i', $response, $m ) ) {
		$classification = strtolower( trim( $m[1] ) );
	}
	if ( preg_match( '/Reason:\s*(.+)/i', $response, $m ) ) {
		$reason = trim( $m[1] );
	}

	if ( ! in_array( $classification, array( 'acceptable', 'likely_spam', 'policy_violation' ), true ) ) {
		$classification = 'acceptable'; // Unrecognized output defaults to the safest, least destructive label.
	}

	return array( 'classification' => $classification, 'reason' => $reason );
}

If the model’s output doesn’t parse cleanly into one of the three known categories, the code defaults to acceptable, the option that results in the least automatic consequence, rather than guessing toward policy_violation.

Step 3: Why this stops at a recommendation

It would be straightforward to add wp_set_comment_status( $comment_id, 'trash' ) when the classification is likely_spam. This project deliberately doesn’t. A model misclassifying a real comment as spam, and that action being taken automatically, silently removes a legitimate reader’s voice with no one aware it happened. Keeping a human in the loop means the AI’s job stops at surfacing a clear, reasoned recommendation, a moderator (or a separate, explicitly human-triggered “apply this recommendation” action) still makes the actual call. This is the same instinct behind the two-step preview/commit split in the Bulk-Editing Assistant project, just applied to moderation instead of content rewrites.

Test it

Add a test comment that reads as obvious spam, then classify it:

wp-env run cli wp comment create --comment_post_ID=1 --comment_content="Buy cheap watches now at totally-legit-watches.example" --comment_author="Spammer"
wp-env run cli wp eval '
$id = get_comments( array( "number" => 1, "orderby" => "comment_ID", "order" => "DESC" ) )[0]->comment_ID;
print_r( wp_get_ability( "content-moderator/classify-comment" )->execute( array( "comment_id" => $id ) ) );
'

Confirm classification returns likely_spam with a reason mentioning the promotional link, then run the same ability against a normal, on-topic comment and confirm it returns acceptable.

Trusting the classification without checking it parsed

If the model doesn’t follow the requested Classification: / Reason: format exactly, for example wrapping its answer in extra commentary, the regex in content_moderator_parse_response() can fail to match and silently fall back to acceptable even for genuinely bad content. Log the raw, unparsed $response somewhere during development so you can see when the model’s output format drifts, and tighten the prompt or the parsing before relying on this in production.

Recap

This ability sends comment content to wp_ai_client_prompt() for classification into one of three fixed categories, logs the classification and reasoning to comment meta for a moderator to see, and returns that same recommendation to the caller. It never calls wp_set_comment_status() or deletes anything itself, moderation action stays a separate, human decision, with the AI’s role limited to surfacing a clear, well-reasoned flag.

Resources & further reading

← Project: Bulk-Editing Assistant Project: Internal-Linking Automation →