Fundamentals

System Prompts and Instructions in the PHP AI Client SDK

⏱ 13 min

Course 5 introduced using_system_instruction() as one method among several on the WP_AI_Client_Prompt_Builder. This lesson treats it as the main subject instead of a footnote, because of everything you can change about a prompt, a clear, well-structured system instruction is the single highest-leverage change you can make for getting consistent behavior out of a model, more so than temperature, more so than a longer user message. This lesson is also the first in a course that isn’t tied to one API surface, everything here applies whether you’re calling wp_ai_client_prompt() directly, inside an ability’s execute_callback, or anywhere else in a WordPress codebase that talks to an LLM.

What you'll learn in this lesson
System content vs user content
Why these are treated differently by the model, and by using_system_instruction() specifically.
The anatomy of a good system instruction
Role, constraints, and expected output format, in that order.
A weak instruction rewritten as a strong one
A concrete before/after for a real WordPress feature.
Keeping instructions stable
Why a system instruction belongs in one place, not copy-pasted at every call site.
Prerequisites

Course 1’s wp_ai_client_prompt() overview, and Course 5’s lesson on generate_text() and the builder’s other methods. This lesson assumes you can already make a working call and get text back, the focus here is entirely on what you put into using_system_instruction(), not on setup.

Step 1: Why system content and user content aren’t the same thing

wp_ai_client_prompt() lets you supply two different kinds of input: the prompt text itself, via with_text(), and a system instruction, via using_system_instruction(). Providers treat these differently under the hood, the system instruction sets standing behavior for the whole exchange, while the prompt text is the specific thing being responded to. Folding both into one long string works, models are perfectly capable of parsing “you are a helpful assistant, now here’s the actual question,” but it’s less reliable in practice, and it gets harder to reuse as your prompt grows. Keeping the two separate means a model reads “who am I and what are my rules” as a distinct block from “what am I being asked to do right now,” which is exactly the distinction you want it to preserve when it generates a response.

// File: my-plugin/class-support-reply-drafter.php
$response = wp_ai_client_prompt()
	->with_text( $ticket_body )
	->using_system_instruction(
		'You draft first-pass replies to customer support tickets for a small ' .
		'WordPress plugin business. Keep replies under 120 words, plain and direct, ' .
		'no marketing language, and never promise a fix timeline you cannot verify.'
	)
	->generate_text();

Step 2: The anatomy of a system instruction that actually works

A system instruction that reliably shapes output tends to cover three things, in this order: who the model is acting as, what it must not do, and what shape the output should take. Leaving any one of these out is where inconsistency creeps in, a model given a role but no constraints will happily improvise past your intent, and a model given constraints but no output shape will vary its formatting call to call even when the wording is fine.

// File: my-plugin/class-support-reply-drafter.php
$system_instruction =
	// Role
	'You are drafting support replies for a WordPress plugin support desk. ' .
	// Constraints
	'Never promise a specific release date. Never confirm a bug exists before it has ' .
	'been reproduced. Do not use exclamation points. ' .
	// Output shape
	'Reply in two short paragraphs: the first acknowledges the issue, the second ' .
	'states the next concrete step.';

Writing it in this order, even just as a habit in your own code, makes it much easier to tell later which part of an inconsistent output is missing. If replies vary in tone, check the role line. If a model keeps hedging on things it shouldn’t, check the constraints. If the paragraph count or structure drifts, check the output shape line.

Step 3: A weak instruction rewritten as a strong one

Here’s the same feature with a vague system instruction, the kind that’s easy to write first and easy to leave unchanged:

// File: my-plugin/class-support-reply-drafter.php (weak version)
->using_system_instruction( 'Be helpful and reply to the support ticket.' )

“Be helpful” gives the model almost nothing to hold onto, no constraint stops it from promising a fix by Friday, no shape stops it from writing one paragraph one time and five bullet points the next. The rewritten version from Step 2 fixes both, and the difference shows up immediately once you run the same ticket text through both.

Test with the same input, instruction only changed

When you’re deciding whether a system instruction rewrite actually helped, hold the user text and every other builder setting constant and change only the instruction. Anything else that changes between runs, temperature, the input itself, makes it impossible to tell what actually caused a difference in output.

Step 4: One instruction, one place

Once a system instruction is doing real work, the last thing you want is three slightly different copies of it scattered across your plugin because someone pasted it into a second function and tweaked a word. Keep it as a single constant or a method that returns it, so every call site that needs this behavior gets the exact same wording:

// File: my-plugin/class-support-reply-drafter.php
class Support_Reply_Drafter {
	private function get_system_instruction(): string {
		return
			'You are drafting support replies for a WordPress plugin support desk. ' .
			'Never promise a specific release date. Never confirm a bug exists before ' .
			'it has been reproduced. Do not use exclamation points. ' .
			'Reply in two short paragraphs: the first acknowledges the issue, the ' .
			'second states the next concrete step.';
	}

	public function draft_reply( string $ticket_body ): string {
		return wp_ai_client_prompt()
			->with_text( $ticket_body )
			->using_system_instruction( $this->get_system_instruction() )
			->using_temperature( 0.3 )
			->using_max_tokens( 220 )
			->generate_text();
	}
}

Lesson 7 takes this one step further, moving the instruction itself out of PHP entirely and into a versioned wp_options entry, but a single private method like this is already a real improvement over copy-pasted strings and worth doing even before you get there.

A system instruction isn't a security boundary

This is worth repeating from Course 5: using_system_instruction() shapes behavior, it does not enforce it the way a permission check does. A model can still occasionally ignore or partially violate an instruction, especially under adversarial input. Never rely on wording alone to prevent a model from producing something unsafe to display, validate and sanitize the output the same way you would any other external input. Lesson 5 in this course covers the real, code-level guardrail for anything you actually need to block.

Test it: compare weak and strong instructions

wp-env run cli wp eval '
$ticket = "The plugin update broke my checkout page and now customers cannot pay.";

echo "WEAK:\n";
echo wp_ai_client_prompt()
	->with_text( $ticket )
	->using_system_instruction( "Be helpful and reply to the support ticket." )
	->generate_text();

echo "\n\nSTRONG:\n";
echo wp_ai_client_prompt()
	->with_text( $ticket )
	->using_system_instruction(
		"You are drafting support replies for a WordPress plugin support desk. " .
		"Never promise a specific release date. Never confirm a bug exists before " .
		"it has been reproduced. Do not use exclamation points. " .
		"Reply in two short paragraphs: the first acknowledges the issue, the " .
		"second states the next concrete step."
	)
	->generate_text();
'

The weak version will vary run to run, sometimes hedging, sometimes promising a timeline, sometimes one paragraph, sometimes several. The strong version should hold its shape and constraints consistently across repeated runs with the same ticket text.

Recap

using_system_instruction() is where standing behavior belongs, separate from the specific text in with_text(). A system instruction that states a role, explicit constraints, and an expected output shape, in that order, produces far more consistent output than a longer or vaguer one. Keep the wording in one place in your code so every call site shares it, and remember it’s a behavioral tool, not a security control.

Resources & further reading

Few-Shot Prompting Patterns →