Operationalizing

Building Audit Trails for Compliance, Not Just Debugging

⏱ 14 min

Course 4 built a custom audit log that answers “which ability did which user call, and did it succeed.” That’s exactly the right log for debugging and security review. A compliance review asks a related but different set of questions: what personal data, if any, left our systems, which external AI provider received it, under what stated purpose, and when. Nothing built into the MCP Adapter or the PHP AI Client SDK answers those questions for you, same as Course 4 found for the debugging case. This lesson extends the same pattern, deliberately, to cover them.

What you'll learn in this lesson
Why a debugging-oriented log and a compliance-oriented log need different fields
Ability name and user ID aren't enough, a compliance reviewer needs provider, purpose, and data category.
Extending Course 4's audit table with compliance-specific columns
provider, purpose, and a data-category flag, added to the existing schema.
Logging PHP AI Client SDK calls, not just ability calls
The direction Lesson 5 covered, WordPress code calling out to a model directly.
Retention and access considerations for the log itself
A compliance log storing what data was sent is itself sensitive, treat it that way.
Prerequisites

Course 4’s “Building Your Own Audit Log for Agent Actions” lesson. This lesson extends that table and wrapper function rather than starting over, read that lesson first if you haven’t already.

Step 1: What a compliance reviewer actually asks that a debug log doesn’t answer

Course 4’s audit table stores ability, user_id, input, error, duration_ms, and a timestamp, exactly right for “did this call succeed, and how long did it take.” A compliance reviewer, asked to demonstrate what happened when a customer requested to know what data an AI feature processed about them, needs a different, overlapping set of answers.

Debugging log vs. compliance log, same table, more columns
QuestionCourse 4’s columnsWhat’s missing
Which action ran, for which user, did it succeed?ability, user_id, error, duration_msNothing, this is already covered.
Which external AI provider received data, if any?Not capturedA provider column.
Why was this data sent, what was the stated purpose?Not capturedA purpose column.
Did the data sent include a personal-data category?Not capturedA data_category flag.

Step 2: Extend the existing table, don’t build a second one

Add the compliance-specific columns to the same table Course 4 created, rather than maintaining two separate logs that can drift out of sync with each other.

// File: my-plugin.php
function my_plugin_extend_audit_table_for_compliance() {
	global $wpdb;
	$table = $wpdb->prefix . 'ai_agent_audit_log';

	require_once ABSPATH . 'wp-admin/includes/upgrade.php';
	// dbDelta() can add new nullable columns to an existing table safely.
	dbDelta( "CREATE TABLE {$table} (
		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
		ability VARCHAR(191) NOT NULL,
		user_id BIGINT UNSIGNED NOT NULL,
		input LONGTEXT NULL,
		error TEXT NULL,
		duration_ms FLOAT NULL,
		provider VARCHAR(64) NULL,
		purpose VARCHAR(191) NULL,
		data_category VARCHAR(64) NULL,
		created_at DATETIME NOT NULL,
		PRIMARY KEY (id),
		KEY ability (ability),
		KEY user_id (user_id),
		KEY provider (provider)
	) {$wpdb->get_charset_collate()};" );
}
register_activation_hook( __FILE__, 'my_plugin_extend_audit_table_for_compliance' );

Step 3: Populate the new fields wherever a prompt is constructed

Course 4’s wrapper logged ability calls. Compliance also needs the direction Lesson 5 covered: WordPress code calling an AI model directly through the PHP AI Client SDK. Wrap that call site the same way, with the new fields filled in deliberately, not inferred:

// File: my-plugin.php
function my_plugin_log_ai_call( array $entry ) {
	global $wpdb;
	$wpdb->insert( $wpdb->prefix . 'ai_agent_audit_log', array(
		'ability'       => $entry['ability'] ?? 'direct_ai_call',
		'user_id'       => get_current_user_id(),
		'input'         => $entry['input'], // Already redacted, per Lesson 5.
		'provider'      => $entry['provider'],
		'purpose'       => $entry['purpose'],
		'data_category' => $entry['data_category'],
		'created_at'    => current_time( 'mysql' ),
	) );
}

function my_plugin_summarize_ticket( int $ticket_id ) {
	$ticket    = my_plugin_get_ticket( $ticket_id );
	$safe_body = my_plugin_redact_pii( $ticket->subject . "\n" . $ticket->body ); // Lesson 5.

	my_plugin_log_ai_call( array(
		'input'         => $safe_body,
		'provider'      => 'anthropic',
		'purpose'       => 'support_ticket_summarization',
		'data_category' => 'support_content',
	) );

	return ai_client()->using_provider( 'anthropic' )->generate_text(
		"Summarize this support ticket in two sentences:\n\n" . $safe_body
	);
}

The purpose field is worth taking seriously, not filling with a generic value. “Why was this data sent” is exactly the question a compliance reviewer, or a user exercising a data-access request, will ask, and a specific, consistent purpose string per feature answers it directly.

Log the redacted input, never the raw one

The same rule from Course 4 applies here with more weight: this table now explicitly documents what left your system for compliance purposes. Storing an unredacted version defeats the entire point, and creates a second place unredacted PII can leak from.

Step 4: Treat the log itself as sensitive

A table recording what personal data was sent, to which provider, and why, is itself a record of personal data processing, which makes the log a thing worth protecting and governing, not just a technical artifact.

Restrict who can query the raw table
Same discipline as any table holding even redacted personal data references.
Define a retention period, and actually enforce it
A compliance log kept forever is its own liability, not just a growing table.
Build one query path a compliance reviewer or DPO could actually run
A saved query or small admin screen answering "what did we send about user X, and why" beats asking them to write SQL.
Include this table explicitly in your organization's data inventory
If your organization maintains a record of processing activities, this table is a data source that belongs in it.

Test it: answer a compliance question from the log

wp-env run cli wp eval '
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare(
  "SELECT ability, provider, purpose, data_category, created_at FROM {$wpdb->prefix}ai_agent_audit_log WHERE user_id = %d ORDER BY id DESC LIMIT 20",
  123
) );
print_r( $rows );
'

If you can run this and get a clear, honest answer to “what AI processing happened involving this user, with which provider, and why,” the log is doing its compliance job, not just its debugging one.

Recap

A debugging-oriented audit log and a compliance-oriented one overlap heavily but aren’t identical. Extending Course 4’s existing table with provider, purpose, and data_category columns, populated deliberately at every point a prompt is constructed (both ability calls and direct PHP AI Client SDK calls), turns one audit trail into something that answers both kinds of question. Treat the resulting table as sensitive in its own right: restrict access, define retention, and make sure someone could actually query it to answer a real compliance question if asked.

Resources & further reading

← Bias and Fairness Considerations in AI-Driven Features Course Recap: A Governance Checklist for Shipping AI Features →