---
url: 'https://adk.nht.io/the-loop/tools/what-a-tool-is.md'
description: >-
  The four ingredients of a Tool — name, description, input schema, handler —
  plus artifactConstructor, meta, and the three behavioural flags.
---

# What a Tool is

[Tools](../tools) covers the overview and the per-section navigation.

A [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) is built out of three things the model sees and one thing the model never sees.

The model sees the **name**, the **description**, and the **input schema**. The name is the identifier the model uses to invoke the tool. Use lowercase snake\_case: it is what every major provider's tool-definition format prefers and what survives round-tripping cleanly. Cute names become provider-specific bugs. The description is the prose the model reads when deciding whether this is the tool it wants — what it does, what it returns, when to reach for it. The input schema is a `@nhtio/validation` schema (object-shaped; the validator enforces that at construction) whose `.describe()` output the [`DispatchExecutorFn`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/type-aliases/DispatchExecutorFn) folds into the provider-specific tool definition: types, field descriptions, examples, notes, the whole annotation surface, captured once and rendered into whatever wire shape the provider expects.

The model does not see the **handler**. The handler is the function that runs when the tool is invoked, and it is deliberately not exposed as a public property on the tool — the only legitimate way to call it is through [`Tool.executor`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#executor)`(ctx)`, which validates the arguments first, computes the stable `callId` the rest of the loop uses to correlate events, fires the start/end observability events, and wraps any thrown error in `E_TOOL_DOWNSTREAM_ERROR` so the failure surfaces through the right channel. A handler that throws on bad arguments is a tool you cannot debug; a handler whose call is not observable is a tool you cannot audit. The executor wrapper exists so both of those problems are solved once, the same way, for every tool.

The handler may return any of four shapes (or a `Promise` of any of them): `string`, `Uint8Array`, [`Media`](../primitives#media), or `Media[]`. `Tool.executor()` itself returns that value raw — persistence and wrapping are the consumer's executor's job. The first two are the *bytes-or-text* path: the executor spools the return value and wraps it via [`Tool.artifactConstructor`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-artifactconstructor)`?.() ??` [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) — the model then interacts with the result through the forged `artifact_*` handle tools, which is what you want for content the model needs to *query* (grep a log, page a JSON tree, walk a Markdown document by heading). The `Media` path is the *explicit-modality* path: when the tool already knows it has produced an image, an audio clip, a video, or a document the provider can render natively, the handler returns one or more [`Media`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media) instances directly and the executor lands them on [`ToolCall.results`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-results) *without* invoking [`Tool.artifactConstructor`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-artifactconstructor). This mirrors the precedent set by [`ArtifactTool`](../artifacts#artifacttool) — when the handler can declare the final result shape, the artifact wrap is skipped. The renderer reaches into each `Media` for bytes only at the wrap site ([`Media.stream`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#stream) for streaming uploads, [`Media.asBytes`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#asbytes) / [`Media.asBase64`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#asbase64) for inline content blocks), so a tool that holds bytes lazily — backed by a fetch, a file handle, an S3 object — keeps them lazy all the way through middleware and persistence.

The handler's return shape is the most consequential choice in writing a tool — it decides whether the model gets the result as a queryable handle or an inline content block. Pick by what the model will *do* with it:

| Return | Best when | What the ADK does | How the model sees it |
| --- | --- | --- | --- |
| `string` / `Uint8Array` | The model needs to **work with** the content — grep, page, query, walk a structure | Spools the bytes; wraps in [`SpooledArtifact`](../artifacts) (or the subclass your `artifactConstructor` resolves); forges `artifact_*` handle tools | Calls the forged handle tools to read the content lazily |
| [`Media`](../primitives#media) / `Media[]` | The provider can **render the bytes natively** (image, audio, document, video) | Lands the instance on [`ToolCall.results`](../primitives#toolcall) untouched; battery emits a provider-specific content block | Sees the asset inline as a content block |

`Media` is not "new artifacts." It is a different silo. A PDF the model must grep is an artifact. A PDF the provider can show inline is a `Media`. Pick wrong and you either hide queryable structure behind a blob or force a native asset through fake text tooling. A tool that produces both is free to return `Media` while staging the text-extracted form on [`Media.stash`](../primitives#media) as a fallback for text-only consumers.

Three more fields shape how the tool behaves once it lands in a registry. `artifactConstructor` ([`SpooledArtifactConstructor`](https://adk.nht.io/api/@nhtio/adk/forge/type-aliases/SpooledArtifactConstructor)) is the [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) subclass to wrap this tool's results in, declared by the tool that produces those results (a tool returning JSON declares [`SpooledJsonArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledJsonArtifact); a tool returning Markdown declares [`SpooledMarkdownArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledMarkdownArtifact); a tool returning plain text leaves the default in place). It is a resolver — a zero-argument closure that returns the constructor — rather than a direct reference; the reason is a module-load cycle covered under [Advanced details](./advanced).  `meta` is a free-form metadata bag the ADK stores in a [`Registry`](https://adk.nht.io/api/@nhtio/adk/common/classes/Registry) for dot-path access: RBAC scopes, feature flags, telemetry hints, whatever your middleware needs to inspect at dispatch time. The ADK does not interpret any of it; that is the entire point.

[`Tool.trusted`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-trusted), [`Tool.ephemeral`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-ephemeral), and [`Tool.onCollision`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-oncollision) are the three flags that change how the tool behaves at the seams: the trust envelope the executor renders the result in, whether the registry treats this tool as a one-dispatch citizen, and how the registry reconciles name clashes during a merge. Each is documented in the section that owns it: [Trust on the tool, not on the battery](./trust-and-safety#trust-on-the-tool-not-on-the-battery), [bindContext and ephemeral pruning](./bind-context-and-describe#bindcontext-and-ephemeral-pruning), and [Collision policy](./registry#collision-policy).

::: warning One mistake worth naming explicitly
Tool handlers normally run inside the **executor** — the [`DispatchExecutorFn`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/type-aliases/DispatchExecutorFn) you registered is the seam that resolves a model-requested tool call and invokes the handler. (A middleware *can* dispatch a tool call too, but that is the unusual path; the executor is where it happens by default.) Either way, it is tempting to capture state from the surrounding seam in the handler's closure. Don't. The handler is called per invocation; the executor is a long-lived seam closed over at runner construction, and a closure-captured value that survives across turns and leaks into a handler running in a later turn is the kind of bug that takes a week to find. Read state from [`TurnContext.stash`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-stash) instead — it is a [`Registry`](https://adk.nht.io/api/@nhtio/adk/common/classes/Registry), so use `ctx.stash.get('your-namespace')` and `ctx.stash.set('your-namespace', value)` (dot-paths work too: `ctx.stash.get('your-namespace.subkey')`). That registry is exactly what cross-middleware scratchpad state is for.
:::
