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\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;
use Drupal\tool\TypedData\OutputDefinition;
#[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 OutputDefinition(
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. Left out, a Write or Trigger tool counts as destructive and a Read tool does not; a write tool that only adds data says FALSE. |
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. |
icon |
no | An icon representing the tool, as a core Icon API full icon ID (pack_id:icon_id). See below. |
permission |
no | A permission the account must hold, in the syntax of a route's _permission requirement. Sufficient for a permission-only tool; a checkAccess() override can add restrictions. See Declared permissions. |
Tool icons¶
A tool can reference an icon from a core Icon API
icon pack (Drupal 11.1+), declared by any module or theme in a *.icons.yml file:
icon: 'my_module:wrench',
The definition stores the reference verbatim; nothing resolves or validates it at
discovery time. Consumers such as the Tool Explorer resolve it through the icon pack
manager where available and treat a missing pack, a missing icon, or an older core as
"no icon". Derivers can set the icon definition property per derivative, so derived
tools (for example per-entity-type tools) can carry distinct icons.
The module ships a tool icon pack with one icon per operation
(tool:operation-read, tool:operation-write, and so on). The Tool Explorer falls
back to the operation icon for tools that declare no icon of their own, and other
UIs may reuse the pack the same way. The fallback is presentation-only: it never
appears on the definition, so machine consumers see exactly what the tool declared.
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¶
A ToolBase plugin either overrides doExecute() or declares a typed
__invoke() (see Typed tools), and needs an access rule:
doExecute(array $values): ExecutableResultruns the tool. It receives the input values keyed by input name and returns a result. UseExecutableResult::success($message, $outputValues)on success andExecutableResult::failure($message)on failure. Output values are keyed by output name and must match your declaredoutput_definitions. The base class catches exceptions fromdoExecute()and turns them into a failure result, so you do not have to wrap everything in try/catch.- Access can be defined with
permissionin the attribute, an override ofcheckAccess(array $values, AccountInterface $account, bool $return_as_object): bool|AccessResultInterface, or both. A permission-only tool can omitcheckAccess(): the default allows access after the declared permission and input validation pass. An override decides on its own authority - the declared permission has already been required before it runs, so it never needs to call the parent or re-check it - and its result is combined with the permission check: both must allow. Overridingaccess()itself also counts as an access rule, for a tool that replaces the whole gate, except on a typed tool with#[ToolEntityInput]parameters (see below). A tool with no declared permission and neither override is rejected at discovery. The greeting example above uses an override to explicitly allow every account.
The two routes are not equivalent for a rule that does not depend on the
input values. Invokers check the declared permission before they process
any caller input (see Invoking a tool), so a
permission denies before transforms resolve handles or refiners run; a
checkAccess() override only runs after the values were resolved and
validated. Declare a value-independent rule as permission and keep
checkAccess() for what genuinely depends on the values.
For an input error, throw Drupal\tool\Exception\InputException with the reason
and inputNames: ['input_name']. Pass strings as plain text. For translated
messages, pass the TranslatableMarkup object directly, without casting it to a
string. The exception converts markup objects to plain text once; its
getMessage() must not be HTML-decoded again. Literal strings such as <p> or
<p> are preserved. Failed results expose one violation per named input
through getViolations(), with the input name in the property path.
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
Typed tools¶
Instead of implementing doExecute() and writing input_definitions and
output_definitions arrays, a plugin can declare a public typed __invoke()
method. The #[Tool] attribute keeps the id, label, description, and
operation. Parameters become inputs, the return value becomes the output, and
the same inference and attributes apply as for
service methods: native and docblock types,
#[ToolInput] and #[ToolEntityInput] on parameters, #[ToolOutput] on
the method, value objects, and shapes. ToolBase::doExecute() calls
__invoke() with the validated values as named arguments.
#[Tool(
id: 'greeting_tool',
label: new TranslatableMarkup('Greeting Tool'),
description: new TranslatableMarkup('Generates a greeting message.'),
operation: ToolOperation::Transform,
permission: 'access content',
)]
final class GreetingTool extends ToolBase {
/**
* Generates the greeting.
*
* @param string $name
* The name to greet.
* @param string|null $greeting
* The greeting to use.
*
* @return string
* The generated greeting message.
*/
#[ToolOutput(name: 'message')]
public function __invoke(string $name, ?string $greeting = NULL): string {
return ($greeting ?? 'Hello') . ", $name!";
}
}
The two styles blend. Any input_definitions or output_definitions the
attribute still declares are kept as written and only the rest are inferred,
so an existing plugin can convert one definition at a time. An attribute
input that is not a parameter of __invoke() fails discovery, and so does a
plugin that neither overrides doExecute() nor declares __invoke(). A
plugin that overrides doExecute() is left as written, even if it also has
a public __invoke() of its own. A
plugin whose typed definitions fail discovery is logged to the tool channel
and left out; the other tools still load. A missing access rule is the
exception: a plugin with no permission and no checkAccess() or access()
override fails discovery for every tool, as any #[Tool] plugin does.
__invoke() may return an ExecutableResult to set its own message, fail
with a FailureCategory, or fail while still returning outputs. It is used
as is, and each output is then declared in output_definitions or with
#[ToolOutput] and a data_type. See
Returning an ExecutableResult.
The operations declared with #[ToolEntityInput] are always checked:
access() requires them on top of the permission and checkAccess(), so a
checkAccess() override cannot drop them. A plugin that declares them and
overrides access() would skip them, so it is logged and skipped at
discovery. The rest of access stays with the plugin. A plugin
with no permission still writes checkAccess(); when an entity operation is
its whole rule, it returns entityInputAccess(), which evaluates the same
operations. When no entity was passed, such as an optional entity input left
empty, entityInputAccess() is neutral, so the call is denied.