Input and output definitions¶
A tool describes the data it accepts and returns with typed definitions. These definitions drive validation, the execute form, config schema, and how the tool is serialized for an AI function or MCP tool.
Input definitions¶
Inputs are declared with InputDefinition, a typed-data definition (it extends
core's DataDefinition). Its constructor takes:
| Argument | Default | Purpose |
|---|---|---|
data_type |
required | The typed data type, for example string, integer, boolean, email, entity. |
label |
required | Human readable label. |
description |
required | What the input is for. |
required |
TRUE |
Whether the input must be provided. |
multiple |
FALSE |
Deprecated: declare a ListInputDefinition instead (see below). |
default_value |
NULL |
Default when none is provided. |
constraints |
[] |
Typed Data validation constraints. |
locked |
FALSE |
Whether the value is fixed and not user editable. |
For structured and list inputs the module provides specialized definitions:
MapInputDefinitionfor an object with named properties.ListInputDefinitionfor a repeated value.EntityInputDefinitionfor an entity reference.
A list is declared with ListInputDefinition: its own constraints, such as
Count, bind the list, and its item definition carries the constraints of each
item. Declaring an input with multiple: TRUE still loads, with a deprecation
notice: the plugin definition converts it to the list it means, and a map does
the same for a property declared that way. Count moves to the list and every
other constraint stays on the item, so the advertised schema does not change,
apart from two things: a default value is no longer advertised on each item,
and for an invoker taking entities as handles, the handle note of an entity list
moves from the array description to the item.
multiple is removed before rc1
(#3583049).
Convert your declarations now.
Required, empty, and NotBlank¶
required means the value is present and not NULL. It says nothing about
size: an empty list ([]) and an empty map ({}) are values, and a required
list or map input accepts them, matching the JSON Schema the tool advertises,
where required only names the keys a caller must send. The same rule holds
for outputs: a required map output may return {}, and "zero results" is
still a result.
When an input must not be empty, declare that:
NotBlankfor "must not be empty", on a string, list, or map. It is advertised asminLength: 1,minItems: 1, orminProperties: 1.Countfor exact bounds on a list's items or a map's properties, advertised asminItems/maxItemsorminProperties/maxProperties.
'nodes' => new ListInputDefinition(
label: new TranslatableMarkup('Nodes'),
description: new TranslatableMarkup('The nodes to export.'),
constraints: ['NotBlank' => []],
item_definition: new EntityInputDefinition('entity:node', ...),
),
Think about the empty value especially where it would replace stored data: a
tool that writes the settings a caller sends should either declare NotBlank
or treat an empty value as "nothing to do", so a caller's {} cannot wipe a
configuration by accident. A multiple declaration's constraints bind each
item, as they always did, and imply nothing for the list itself: to require
a non-empty list, declare NotBlank or Count on the list
(#3583069).
Example values¶
An input definition can carry illustrative example values, emitted as the JSON
Schema examples keyword next to default when the definition is serialized.
Examples help a non-deterministic caller (an LLM function call, an MCP client)
produce well-formed values for inputs whose shape is not obvious from the type
and description alone. Declare them in the #[Tool] attribute:
'field_name' => new InputDefinition(
data_type: 'string',
label: new TranslatableMarkup('Field name'),
description: new TranslatableMarkup('The machine name of the field.'),
examples: ['field_tags', 'field_summary'],
),
setExamples() sets them on an already built definition. Examples always
describe a single value: a list input declares them on its item definition, they
are emitted on the item schema (items.examples) and the array level stays
clean.
Examples are annotations, not validation constraints: they are emitted verbatim
and never validated or stripped at runtime. They are authored against the
definition as declared — an event subscriber or refiner that re-types an input
in place is responsible for updating or clearing its examples, the same
contract that applies to default_value. A subscriber that swaps in a whole
replacement definition sheds them automatically.
Media types¶
A string input or output can say what it contains with the MediaType
constraint. The definition is serialized with the JSON Schema
contentMediaType keyword, so a caller can tell a summary that carries HTML
from a title that is plain text:
'summary' => new InputDefinition(
data_type: 'string',
label: new TranslatableMarkup('Summary'),
description: new TranslatableMarkup('The summary, as HTML.'),
constraints: ['MediaType' => ['type' => 'text/html']],
),
Like examples, MediaType is an annotation: it accepts every value and
checks nothing. It describes the string's own content, not the resource a URI
points to, and only string schemas get the keyword. text/html says a string
carries markup, not that the markup is safe or was filtered. An input marked
text/html arrives exactly as the caller sent it, and an output carries
whatever your tool returned. Filter it before you render it, through a text
format or Xss::filterAdmin().
Output definitions¶
Outputs are declared with OutputDefinition. It mirrors InputDefinition: the
constructor takes the same arguments in the same order, minus locked, and
description is required. multiple is deprecated here as well: an output or
map property declared with multiple: TRUE converts to a ListOutputDefinition
with the same deprecation notice, so declare the list instead. An output can
carry a media type too; text/html on an output is still not
safe to render unfiltered.
required on an output is key presence: the tool must set it. nullable
says whether null is a value the output carries. It defaults to FALSE, so
null means absence: an optional output set to NULL is left out of the
result and reads back as its default, and a required one set to NULL is
missing. An output that is always sent but holds null when there is no value,
such as a photo's EXIF date or a timestamp that has not happened yet, declares
required: TRUE, nullable: TRUE: it is listed in the parent's required
keys and its type gains null. required: FALSE, nullable: TRUE may be
missing or null. The same rule applies inside a map: a null in an optional
property is left out of the sent value unless the property is nullable. A
list item cannot be left out without moving the items after it, so a null
item is a violation unless the item definition is nullable. Inputs keep
their own rule for now: an optional input sent as null reads as its
default, and an optional input that must not be null declares NotNull.
Declaring an output with a core ContextDefinition or
EntityContextDefinition - or with an input definition class - still loads,
with a deprecation notice. The plugin definition converts it to the matching
*OutputDefinition class, a multiple: TRUE one to a ListOutputDefinition of
its data type, and synthesizes a description from the label when none is set.
This acceptance shim is temporary
(#3583050
removes it before rc1). Convert your declarations now.
For structured and list outputs the module provides matching definitions:
MapOutputDefinitionfor an object with named properties. Every property must be anOutputDefinitionInterface.ListOutputDefinitionfor a repeated value.EntityOutputDefinitionfor an entity reference.
Each output your tool returns from doExecute() must have a matching definition,
keyed by the same name. The runtime sets only the output values that correspond to
a declared definition; extra values in the result are not exposed as outputs, and
output transforms are not applied to them.
The type system: adapters¶
Each data type is mapped to a typed data adapter, a plugin under
Plugin/tool/TypedData/Adapter/. An adapter knows how to render a form element for
the type, how to extract a submitted value, and how to build a config schema
definition for it. The module ships adapters for:
| Adapter | Types it handles |
|---|---|
StringAdapter |
Strings. |
TextAdapter |
Long text. |
NumberAdapter |
Integers and floats. |
BooleanAdapter |
Booleans. |
EmailAdapter |
Email addresses. |
SelectAdapter |
A value from a fixed set of options. |
ListAdapter |
Lists of values. |
MapAdapter |
Structured maps. |
EntityAdapter |
Entity references. |
UndefinedAdapter |
Fallback for types without a specific adapter. |
To support a new data type, add an adapter plugin with the #[TypedDataAdapter]
attribute. The plugin.manager.tool.typed_data_adapter manager resolves the adapter
for a given data definition.
Config schema¶
Adapters also produce the config schema for a tool's inputs. The
Tool Explorer view page uses this to generate a
suggested tool.plugin.<id> schema when one is missing. The suggested root mapping
carries the FullyValidatable constraint, so once you add the schema Drupal
validates stored input values against their constraints.
Refining inputs dynamically¶
A tool can adjust its input definitions based on the values of other inputs. To do
this, pass input_definition_refiners to the #[Tool] attribute and implement
InputDefinitionRefinerInterface. Its refineInputDefinition($name, $definition, $values)
method receives the input being refined and the current values, and returns the
refined definition. The attribute enforces the interface: declaring refiners on a
class that does not implement it throws at definition build time.
To replace the definition of a list input, return a ListInputDefinition. A
refined definition declaring multiple: TRUE still converts to the list it means,
with a deprecation notice, the same way a declaration does.