Skip to content

Creating a tool

A tool is a plugin. Put the class under src/Plugin/tool/Tool/ in your module, give it the #[Tool] attribute, and extend ToolBase.

A minimal tool

<?php

declare(strict_types=1);

namespace Drupal\my_module\Plugin\tool\Tool;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\tool\Attribute\Tool;
use Drupal\tool\ExecutableResult;
use Drupal\tool\Tool\ToolBase;
use Drupal\tool\Tool\ToolOperation;
use Drupal\tool\TypedData\InputDefinition;

#[Tool(
  id: 'greeting_tool',
  label: new TranslatableMarkup('Greeting Tool'),
  description: new TranslatableMarkup('Generates a greeting message.'),
  operation: ToolOperation::Transform,
  input_definitions: [
    'name' => new InputDefinition(
      data_type: 'string',
      label: new TranslatableMarkup('Name'),
      description: new TranslatableMarkup('The name to greet.'),
    ),
    'greeting' => new InputDefinition(
      data_type: 'string',
      label: new TranslatableMarkup('Greeting'),
      description: new TranslatableMarkup('The greeting to use.'),
      required: FALSE,
    ),
  ],
  output_definitions: [
    'message' => new ContextDefinition(
      data_type: 'string',
      label: new TranslatableMarkup('Message'),
      description: new TranslatableMarkup('The generated greeting message.'),
    ),
  ],
)]
final class GreetingTool extends ToolBase {

  protected function doExecute(array $values): ExecutableResult {
    $greeting = $values['greeting'] ?? 'Hello';
    return ExecutableResult::success(
      new TranslatableMarkup('Greeting generated.'),
      ['message' => "$greeting, {$values['name']}!"],
    );
  }

  protected function checkAccess(array $values, AccountInterface $account, bool $return_as_object = FALSE): bool|AccessResultInterface {
    return $return_as_object ? AccessResult::allowed() : TRUE;
  }

}

The attribute

#[Tool] accepts:

Argument Required Purpose
id yes Plugin ID. Must equal the group or be prefixed with group:. For a plain tool, keep the ID and group equal (for example send_email). For a derived tool, prefix it (for example entity:node).
label yes Human readable name.
description yes What the tool does. Callers and models rely on this.
operation yes A ToolOperation case. See below.
destructive no Whether the tool is destructive. Callers use it to prompt for confirmation.
input_definitions no Inputs the tool accepts. See Input and output definitions.
input_definition_refiners no Config for dynamically refining inputs. Requires the class to implement InputDefinitionRefinerInterface.
output_definitions no Outputs the tool returns.
deriver no A deriver class, to produce many tools from one definition.
forms no Form class overrides keyed by operation. Defaults are provided for configure and execute.

Operations

The operation describes the nature of the tool. It drives whether the tool modifies state and whether it is idempotent, which callers use to decide how to treat it.

Operation Modifies state Idempotent Use for
Explain no yes Returning structure or schema, not data.
Read no yes Fetching existing data without changing it.
Transform no no Deriving a result from input, without persisting.
Trigger yes no Starting a process, queue, cron, or webhook.
Write yes no Creating, updating, or deleting stored data.

Implementing the tool

ToolBase requires two methods:

  • doExecute(array $values): ExecutableResult runs the tool. It receives the input values keyed by input name and returns a result. Use ExecutableResult::success($message, $outputValues) on success and ExecutableResult::failure($message) on failure. Output values are keyed by output name and must match your declared output_definitions. The base class catches exceptions from doExecute() and turns them into a failure result, so you do not have to wrap everything in try/catch.
  • checkAccess(array $values, AccountInterface $account, bool $return_as_object): bool|AccessResultInterface decides access. It is abstract, so every tool must implement it and access is always an explicit opt in.

You can also override checkRequirements(): void to assert static prerequisites, such as an API key being configured. Throw a Drupal\tool\Exception\RequirementsException with a message describing what is missing. This is for static configuration, not runtime state like network availability.

Discovery

Tools are discovered by the plugin.manager.tool manager from the Plugin/tool/Tool namespace. After adding or changing a tool, rebuild caches:

drush cr

Then confirm it is registered:

drush tool:info greeting_tool