05 – Give AI Agents Code They Don’t Have to Invent

Give AI Agents Code They Don't Have to Invent

In the last article we added AGENTS.md to FictionalWPSEO, and one of the key instructions was: use the documented public API, don’t directly modify internal post meta or classes. Good advice, but it assumes something important: that our public API is actually good enough to use. If it isn’t, both the agent and a human developer are stuck guessing.

The agent has to choose a path

Say someone asks an agent: “whenever a property is published, set its SEO title to ‘[Property Name] for Sale in [City]’.” FictionalWPSEO is installed. The agent starts investigating and finds several possible ways to update the title: an internal meta key _fwpseo_title, a function fwpseo_save_title(), a class FictionalWPSEO\Internal\MetaRepository, and a filter fictionalwpseo_title. All four might appear to work. Only one may actually be part of our supported public API. If we don’t make that obvious, we’re forcing the agent to guess, and guessing is where fragile generated code starts.

Not really an AI-specific problem

Worth saying early: if your plugin has an inconsistent, undocumented developer API, human developers hit exactly the same wall. They inspect your code, search Google, dig through support threads, find a private meta key, and write update_post_meta( $post_id, '_fwpseo_title', $title ). Six months later you change your internal storage and their integration breaks. Agents just make this existing problem more visible. So the goal isn’t a special “AI API.” It’s a stable, predictable public API for the plugin, full stop.

Designing FictionalWPSEO’s API

We could expose:

fictionalwpseo_get_meta();
fictionalwpseo_update_meta();
fictionalwpseo_delete_meta();

Already there’s a pattern a developer, or an agent, can infer. Compare that to fwp_get_seo(), fictional_save_metadata(), FictionalSEO()->meta()->remove(), seo_update_post(). All might work, but there’s no obvious system. Predictability matters.

Start with a small public surface

Plugins sometimes expose almost everything: every helper becomes public, every class instantiable, every database method callable. That gives flexibility at the cost of a huge compatibility burden. I’d rather expose a small, deliberate API: SEO metadata (get_meta, update_meta, delete_meta), redirects (get_redirect, create_redirect, update_redirect, delete_redirect), Schema (get_schema, update_schema), sitemaps (get_sitemap_settings, update_sitemap_settings). The plugin internally can have hundreds of classes. Third-party developers don’t need access to all of them, just a stable doorway.

One function can beat ten

We could create set_title(), set_description(), set_canonical(), set_robots_index(), set_robots_follow(), then add Open Graph title, description, image, Twitter title, description, image, and suddenly we have dozens of functions. Instead:

fictionalwpseo_update_meta(
    $post_id,
    [
        'title'       => 'SEO Title',
        'description' => 'SEO description',
        'canonical'   => '<https://example.com/page/>',
        'robots'      => [
            'index'  => true,
            'follow' => true,
        ],
    ]
);

The interface can grow while keeping the same basic shape, and it gives developers and agents one mental model: a post ID and a set of SEO properties to update.

Make data structures predictable

If fictionalwpseo_get_meta( $post_id ) returns:

[
    'title'       => 'Luxury Apartment in Chandigarh',
    'description' => 'View price, floor plan and property details.',
    'canonical'   => '<https://example.com/property/luxury-apartment/>',
    'robots'      => [ 'index' => true, 'follow' => true ],
]

then updating should use essentially the same shape. The agent doesn’t need one representation for reading and a completely different one for writing.

Be explicit about partial updates

If we call fictionalwpseo_update_meta( $post_id, [ 'title' => 'New SEO Title' ] ), does it update only the title, or replace all SEO metadata and wipe the fields we didn’t include? Both are valid designs, but the developer shouldn’t have to guess. I’d choose partial updates and document it plainly: fictionalwpseo_update_meta() updates only the properties supplied; existing properties not included remain unchanged. One sentence, and it prevents a lot of mistakes.

Use WordPress types developers already know

Unless there’s a good reason not to, use WP_Error rather than inventing a custom error system:

$result = fictionalwpseo_update_meta(
    $post_id,
    [ 'title' => 'Luxury Apartment in Chandigarh' ]
);

if ( is_wp_error( $result ) ) {
    error_log( $result->get_error_message() );
}

true on success, WP_Error on failure, with codes like invalid_post, unsupported_post_type, invalid_title, invalid_canonical_url, permission_denied. Error handling now follows conventions developers already know.

Validation lives inside the API

This is part of why we don’t want agents writing directly to post meta. If someone calls update_meta with canonical => 'not a valid URL', our API should catch that rather than assuming every caller already validated. Same with a robots.index value of 'maybe'. The public API is a boundary, use it to protect the plugin’s internal state.

Sanitization and escaping are different jobs

Our API might sanitize a title with sanitize_text_field() before storage. That doesn’t mean developers stop escaping output. If a value later renders in HTML, it still needs the right escaping for that context. Documentation should reinforce normal WordPress security practices rather than implying that calling our API makes everything safe everywhere downstream.

Don’t make the agent know your database

Say FictionalWPSEO initially stores metadata in wp_postmeta, then later introduces a dedicated wp_fictionalwpseo_index table for performance. If integrations used fictionalwpseo_get_meta(), nothing needs to change on their end. The public API stays the same interface regardless of what’s underneath: wp_postmeta today, an SEO repository backed by a new table tomorrow. That’s one of the biggest benefits of a real abstraction layer, freedom to change the implementation.

Document every public function properly

For fictionalwpseo_update_meta(), answer: what does it do, what parameters does it accept, what keys can $data contain, what does it return, does it replace or merge, what permissions are required, what hooks fire, which version introduced it. The goal is removing uncertainty, not writing more words for its own sake.

Start simple, then build up

Don’t open documentation with the most impressive example. Start with:

fictionalwpseo_update_meta(
    $post_id,
    [ 'title' => 'Custom SEO Title' ]
);

then build toward something fuller, then a complete WordPress integration. Simple examples help both humans and AI systems understand the pattern before dealing with edge cases.

A real WordPress example

Requirement: automatically set the SEO title when a property is saved.

add_action(
    'save_post_property',
    function ( $post_id, $post, $update ) {

        if ( wp_is_post_revision( $post_id ) ) {
            return;
        }

        $city = get_post_meta( $post_id, 'property_city', true );

        $title = sprintf(
            '%s for Sale in %s',
            get_the_title( $post_id ),
            $city
        );

        $result = fictionalwpseo_update_meta(
            $post_id,
            [ 'title' => $title ]
        );

        if ( is_wp_error( $result ) ) {
            error_log( $result->get_error_message() );
        }
    },
    10,
    3
);

Now the agent has a reliable reference implementation instead of inventing an integration from scratch.

Hooks should be predictable too

The public API is only half the developer interface. Suppose developers can modify the final SEO title through apply_filters( 'fictionalwpseo_meta_title', $title, $post_id ). If our hooks are actually fwpseo_title, fictional_description_filter, seo_canonical_url, fwp_schema_product_data, fictionalwpseo/robots, they might all work individually, but the naming system is inconsistent. Establish conventions early: fictionalwpseo_meta_title, fictionalwpseo_meta_description, fictionalwpseo_canonical_url, fictionalwpseo_schema_article, fictionalwpseo_schema_product, fictionalwpseo_sitemap_entry, fictionalwpseo_redirect_target. Now a developer can recognize these all belong to the same plugin.

Actions should describe events

do_action( 'fictionalwpseo_after_meta_update', $post_id, $data ) or do_action( 'fictionalwpseo_redirect_created', $redirect_id, $redirect ) tell you when the action occurs just from the name. Less ambiguity everywhere.

Don’t make everything a hook

If a developer wants to update SEO metadata, give them an explicit function. If they want to alter the generated title during rendering, a filter makes sense. Performing an operation belongs in the PHP API. Modifying plugin behavior belongs in an action or filter. That distinction alone produces a much clearer developer experience.

Don’t forget capability checks

If FictionalWPSEO exposes administrative operations, think carefully about who can perform them. A REST request that changes site-wide SEO configuration shouldn’t succeed just because the endpoint exists, and content operations should respect the appropriate WordPress capabilities. This matters even more once we get to agent-operated interfaces later in the series: an agent doesn’t get special privileges because it’s an agent.

Keep reads separate from writes

fictionalwpseo_get_meta( $post_id ) reads. fictionalwpseo_update_meta( $post_id, $data ) writes. Avoid a single ambiguous function like fictionalwpseo_meta( $post_id, $data = null ) where behavior changes depending on whether $data is passed. Shorter isn’t always clearer.

Think about idempotency

If an agent calls fictionalwpseo_update_meta( 123, [ 'title' => 'Luxury Apartment in Chandigarh' ] ) twice, the second call shouldn’t create duplicate records or leave the system in an unexpected state. Updating a resource with the same value twice should leave it in the same final state both times. Good API design in general, more valuable still in automated workflows.

Version your API mentally from day one

Once fictionalwpseo_update_meta( $post_id, $data ) is public, someone depends on it, so changing the signature to fictionalwpseo_update_meta( $data, $post_id, $context ) in the next version is a real problem. Once an interface is public, treat it as a contract: preserve backward compatibility where practical, deprecate clearly, document replacements, give migration examples, record the change in the changelog.

Deprecation needs to be machine-obvious too

Don’t just quietly stop mentioning an old function. Mark it with _deprecated_function( __FUNCTION__, '3.0.0', 'fictionalwpseo_update_meta' ) and update the docs: “deprecated since 3.0, use fictionalwpseo_update_meta() instead.” Now if an agent encounters the old function in some historical code, there’s a clear path forward.

A good API reduces hallucination

If someone asks “using FictionalWPSEO, change the canonical URL for post 123” and our documentation is vague, an agent might invent fictionalwpseo_set_canonical_url( 123, '<https://example.com/new-url/>' ), which looks entirely believable but may not exist. If our public API is consistently documented as fictionalwpseo_update_meta( 123, [ 'canonical' => '<https://example.com/new-url/>' ] ), the agent has a strong known pattern to follow instead of a reason to guess.

A library of copy-pasteable examples

I’d build out real, common tasks: update SEO title, update meta description, set canonical URL, set robots directives, modify title through a filter, add Schema properties, modify WooCommerce Product Schema, exclude a post from the sitemap, create a redirect, retrieve SEO metadata through REST, update SEO when an ACF field changes. That last one is especially useful in WordPress:

add_action(
    'acf/save_post',
    function ( $post_id ) {

        if ( 'property' !== get_post_type( $post_id ) ) {
            return;
        }

        $location = get_field( 'property_location', $post_id );

        fictionalwpseo_update_meta(
            $post_id,
            [
                'title' => sprintf(
                    '%s | Property in %s',
                    get_the_title( $post_id ),
                    $location
                ),
            ]
        );
    },
    20
);

Now an agent working on an ACF-based project has a directly relevant pattern.

Document by intent, not just alphabetically

Beyond a function reference, add developer guides organized around what someone’s actually trying to do: how to change an SEO title from PHP, how to generate metadata from ACF fields, how to customize SEO for a custom post type, how to modify WooCommerce Product Schema, how to create redirects programmatically. Developers, and AI prompts, usually start from a goal, not a function name. They ask “how do I automatically create SEO titles for my property post type,” not “show me documentation for fictionalwpseo_update_meta().”

Let the code explain itself too

Website documentation matters, but installed code should carry its own explanation. Proper PHPDoc on public functions:

/**
 * Update SEO metadata for a WordPress post.
 *
 * Only supplied properties are updated. Existing values for
 * omitted properties remain unchanged.
 *
 * @since 3.0.0
 *
 * @param int   $post_id WordPress post ID.
 * @param array $data {
 *     SEO metadata to update.
 *
 *     @type string $title       SEO title.
 *     @type string $description Meta description.
 *     @type string $canonical   Canonical URL.
 *     @type array  $robots      Robots directives.
 * }
 *
 * @return true|WP_Error True on success, WP_Error on failure.
 */
function fictionalwpseo_update_meta( $post_id, array $data ) {
    // ...
}

Now even an agent inspecting the installed plugin directly, rather than the website, can understand the interface.

The API is becoming the product

FictionalWPSEO started as “WP admin, then the plugin underneath.” It’s now closer to a hub with WP admin, the PHP API and hooks all sitting around a shared SEO engine, with the admin interface just one way to interact with it. That’s a stronger architecture, and it sets up what’s coming later in the series: REST, OpenAPI, agents, MCP. All easier once the core plugin already exposes clean, predictable operations.

None of this is exclusively for AI, a WordPress agency building custom integrations benefits from exactly the same work. So instead of asking “what special AI file should I add,” the better question is: if software had to understand and use my plugin without asking me questions, have I actually made that possible?

Next: Does Your WordPress Plugin Need llms.txt?