doExecute and ExecutableResult
When to Use
Use this when writing the body of a tool.
doExecute()is the only abstract method onToolBase; it receives resolved, validated values and returns anExecutableResult.Version: applies to
drupal/tool1.0.0-beta11 (beta; no security advisory coverage). Paths are undermodules/contrib/tool/.
Pattern
use Drupal\tool\ExecutableResult;
use Drupal\tool\FailureCategory;
// tool 1.0.0-beta11 src/Tool/ToolBase.php
abstract protected function doExecute(array $values): ExecutableResult;
// tool 1.0.0-beta11 src/ExecutableResult.php
public static function success(TranslatableMarkup $message, ?array $context_values = []): static
public static function failure(TranslatableMarkup $message, ?array $context_values = [], FailureCategory $category = FailureCategory::Input): static
$context_values is keyed by output name. ExecutableResult is final readonly. Results are not cached by design: ToolInterface is "Deliberately not cacheable" (#3582965). Cache inside doExecute() with Drupal's cache API when a Read tool is expensive, and give list tools paging inputs with Range constraints, as Tool Belt's entity_list does with amount and offset.
Failure Categories
FailureCategory tells a caller whether retrying with different arguments can help (tool 1.0.0-beta11 src/FailureCategory.php).
| Case | Meaning | isCorrectable() |
|---|---|---|
Input |
Input values were invalid, missing or unsuitable | TRUE |
Access |
The user may not run the operation | FALSE |
Runtime |
Failed for a reason unrelated to the input | FALSE |
failure() defaults to Input. The AI connector attaches the input schema to correctable failures so the model retries (tool 1.0.0-beta11 modules/tool_ai_connector/src/Plugin/AiFunctionCall/ToolPluginBase.php).
What Reaches the Caller When doExecute Throws
execute() catches every exception from doExecute() and value resolution (tool 1.0.0-beta11 src/Tool/ToolBase.php):
doExecute() throws... |
Caller sees | Category |
|---|---|---|
\InvalidArgumentException or InputException |
"Tool execution failed due to invalid input: " plus the exception message | Input |
| Anything else | "Tool execution failed: tool log channel |
Runtime |
The same \InvalidArgumentException path also catches input validation failures, because getExecutableValues() throws one with the violation list.
Decision
| If... | Do... |
|---|---|
| The caller can fix it by changing arguments | ExecutableResult::failure($msg) (category Input) |
| The account lacks rights discovered late | ExecutableResult::failure($msg, [], FailureCategory::Access) |
| A service, network or bug failed | ExecutableResult::failure($msg, [], FailureCategory::Runtime) |
| You hit an unexpected exception | Let it propagate; ToolBase logs it and hides the detail |
Common Mistakes
- Catching
\Exceptionand returningfailure()with$e->getMessage()→ this sends internals (paths, SQL, service names) to an LLM or MCP client. The generated template (src/Drush/Generators/tool.twig) and several Tool Belt tools do this; curate the message - Returning
failure()for a runtime fault without a category → the defaultInputtells a model to retry with other arguments, wasting calls - Reading
getResult()beforeexecute()→\BadMethodCallException; usehasResult()andhasExecuted() - Putting internals in
context_valueson a failure → they never become typed outputs, butgetFormattedResult()still carries them: Drush prints them and the AI connector returns them inoutputswithsuccess: false. Only the MCP bridge drops them (mcp_server_tool_bridge 1.0.0-beta3src/Plugin/mcp_server/Tool/ToolApi.php)
See Also
- Output Definitions → how values become outputs
- Security Checklist
- Reference:
modules/contrib/tool/src/ExecutableResult.php,modules/contrib/tool/src/FailureCategory.php,modules/contrib/tool/src/Tool/ToolBase.php