,

Function Calling with Gemini: Letting AI Manage WordPress Tasks Safely

Function calling turns Gemini from a text generator into an operator that can act on your site. Here's the full round-trip in PHP, and the allowlist-based safety architecture that makes "AI runs my maintenance" sane.

Everything so far in this series has Gemini producing text. Function calling is the step change: the model can now decide to act (“check which plugins have updates,” “get the backup status”) by calling functions you define. This is the primitive underneath every AI agent, and it’s also where careless design gets genuinely dangerous, because you’re wiring a probabilistic system to real operations on a real site.

This post covers both halves: the mechanics of the function-calling round-trip in PHP, and the safety architecture that makes it responsible. The second half is the part that matters.

How function calling actually works

A common misconception: the model does not execute anything. The round-trip is:

  1. You send a prompt plus declarations of available functions (name, description, JSON Schema parameters).
  2. The model responds with a functionCall part (a function name and arguments) instead of text.
  3. Your code decides whether to execute, executes, and sends the result back as a functionResponse part.
  4. The model uses the result to continue, possibly another call, eventually a text answer.

Your code sits between every model decision and every real effect. That position is your entire security model: never delegate it.

Declaring functions

$tools = [ [
    'functionDeclarations' => [
        [
            'name'        => 'list_plugin_updates',
            'description' => 'Lists installed plugins that have an update available, with current and new version.',
            'parameters'  => [ 'type' => 'object', 'properties' => new stdClass() ],
        ],
        [
            'name'        => 'get_site_health',
            'description' => 'Returns site health summary: WordPress version, PHP version, disk space, cron status.',
            'parameters'  => [ 'type' => 'object', 'properties' => new stdClass() ],
        ],
        [
            'name'        => 'update_plugin',
            'description' => 'Updates a single plugin to its latest version. Only call when the user explicitly asked for an update.',
            'parameters'  => [
                'type'       => 'object',
                'properties' => [
                    'slug' => [ 'type' => 'string', 'description' => 'Plugin slug, e.g. "akismet".' ],
                ],
                'required'   => [ 'slug' ],
            ],
        ],
    ],
] ];

Descriptions are prompt engineering: the model chooses functions based on them. Note the instruction embedded in update_plugin‘s description: behavioral constraints belong in the declaration itself, though as we’ll see, never only there.

The round-trip loop

function wpai_run( string $user_request, int $max_turns = 6 ): string {
    $contents = [ [ 'role' => 'user', 'parts' => [ [ 'text' => $user_request ] ] ] ];

    for ( $turn = 0; $turn < $max_turns; $turn++ ) {
        $data = wpai_call_gemini( $contents ); // wraps generateContent with 'tools' attached
        $parts = $data['candidates'][0]['content']['parts'] ?? [];
        $calls = array_filter( $parts, fn( $p ) => isset( $p['functionCall'] ) );

        if ( ! $calls ) {
            return $parts[0]['text'] ?? ''; // model is done; final answer
        }

        $contents[] = [ 'role' => 'model', 'parts' => array_values( $calls ) ];

        foreach ( $calls as $call ) {
            $name   = $call['functionCall']['name'];
            $args   = $call['functionCall']['args'] ?? [];
            $result = wpai_execute( $name, $args ); // the safety gate, see below

            $contents[] = [
                'role'  => 'user',
                'parts' => [ [ 'functionResponse' => [
                    'name'     => $name,
                    'response' => [ 'result' => $result ],
                ] ] ],
            ];
        }
    }
    return 'Stopped: too many function-call turns.';
}

The $max_turns cap is not optional. A loop that keeps calling functions until the model feels finished is an unbounded loop attached to your API bill and your site.

The safety gate

Here is the architecture that separates a useful tool from an incident report. Three layers:

Layer 1: an allowlist, not a shell. Every function maps to a specific, hand-written PHP implementation. There is no run_command function, no string that reaches exec(). The model picks from a menu; it cannot order off-menu. If you’re tempted to add a generic shell function “just for flexibility,” you’ve replaced your security model with the model’s judgment plus whatever ended up in its context window.

Layer 2: read/write separation with confirmation. Classify every function. Reads execute freely; writes return a confirmation request instead of executing:

const WPAI_READ_FUNCS  = [ 'list_plugin_updates', 'get_site_health' ];
const WPAI_WRITE_FUNCS = [ 'update_plugin' ];

function wpai_execute( string $name, array $args ): array {
    if ( in_array( $name, WPAI_READ_FUNCS, true ) ) {
        return wpai_dispatch( $name, $args );
    }
    if ( in_array( $name, WPAI_WRITE_FUNCS, true ) ) {
        if ( empty( $args['confirmed_by_user'] ) ) {
            return [
                'status'  => 'confirmation_required',
                'message' => 'Ask the user to confirm this action, then retry with confirmed_by_user set.',
                'action'  => $name,
                'args'    => $args,
            ];
        }
        return wpai_dispatch( $name, $args );
    }
    return [ 'status' => 'error', 'message' => 'Unknown function.' ];
}

In a real UI, confirmation_required renders as a button the human clicks: the flag is set by your interface code after a real click, never by the model. Destructive operations (delete anything, modify users, change options) shouldn’t even be in the declaration list; absence is stronger than a guardrail.

Layer 3: capabilities and audit. Run every dispatch through the current user’s actual WordPress capabilities (current_user_can( 'update_plugins' ) before updating a plugin; the AI never has more power than the human driving it), validate arguments against your own schema before use (treat model output like form input: sanitize, allowlist slugs against installed plugins), and log every call (function, args, user, outcome) to an audit table. When something surprising happens, the audit log is the difference between a fix and a mystery.

Where this connects to WordPress core

If you read the WordPress 7.0 post in this series, this architecture should look familiar: it’s exactly what the core Abilities API formalizes: named actions with JSON Schema inputs and permission callbacks, consumable by AI through the core client’s usingFunctionDeclarations() and using_abilities(). On 7.0+, register your functions as abilities and you get the allowlist, the schema validation, and the permission layer as a framework instead of hand-rolling it. The mental model in this post is what makes you use that framework well.

Next in the series: structured output, making Gemini emit schema-valid JSON, and using it to generate real Gutenberg blocks.

The same allowlist idea works one level up, on the coding agent that runs your automation: Codex scheduled tasks and hooks: running unattended checks safely has a hook that blocks destructive shell commands before they execute.

This post is part of the Gemini and WordPress developer guide, which lists every post in the series.

Aditya Shah Avatar

Written by

Hosting Support Manager & DevOps Engineer at WPMU DEV. I scale and secure WordPress infrastructure, and organize WordPress Bhopal and GDG Cloud Bhopal.

More about me · LinkedIn · GitHub