# Actions vs Tools

TnsAI has three overlapping extension points. They are not interchangeable.

| Primitive | Where it lives | Who calls it | Schema for the LLM | Typical trigger |
|-----------|----------------|--------------|--------------------|-----------------|
| `@ActionSpec` on a `Role` method | Role class | `ActionExecutor` (`executeAction` / role routing) | Not a function schema. An `LLM` action can *use* registered tools; the action method itself is not one | Deterministic, typed work the agent (or your code) invokes by name |
| `@Tool` on a POJO method | Any POJO you register | `ToolMethodDispatcher` during an LLM tool-call loop | Yes — method name + `@ToolParam` become the JSON schema | The model chooses a function at generation time |
| `BuiltInTool` enum constant | `tnsai-tools` catalog | Same dispatcher, after `AgentBuilder.builtInTools(...)` instantiates the POJO | Yes — every `@Tool` on the backing class | You want a shipped toolkit instead of writing one |

`BuiltInTool` is not a third runtime. It is a compile-safe index of shipped `@Tool` POJOs (`BuiltInTool.java`). A ServiceLoader replacement is tracked as TAN-2977 — do not treat the enum as the forever registration API.

## Decision

Use **`@ActionSpec`** when the call must be typed, deterministic, and routed by `ActionType` (`LOCAL`, `WEB_SERVICE`, `LLM`, `MCP_TOOL`). Approvals, contracts, and resilience hang off this path. See [Action System](action-system.md).

Use **`@Tool`** when the LLM should see a function and pick it. Register the POJO with `AgentBuilder.toolPojos(...)`. See [Custom Tools](../../capabilities/tools/custom-tools.md).

Use **`BuiltInTool`** when that function already ships in `tnsai-tools`. Register the enum constant; do not re-wrap the POJO in an action. See [Catalog](../../capabilities/tools/catalog.md).

## Same domain, three primitives

Forecast lookup as a local action, a custom tool, and a shipped toolkit. `AgentBuilder.build()` still needs the [accountability trio](../../security/accountability.md); the snippets below omit it.

### 1. Local `@ActionSpec` — your code calls it

```java
public class WeatherRole extends Role {
    @Override public RoleIdentity getIdentity() {
        return new RoleIdentity("weather", "Looks up forecasts", "ops");
    }

    @ActionSpec(type = ActionType.LOCAL, description = "Return a stored forecast")
    public String forecast(String city) {
        return weatherService.forecast(city);
    }
}

ActionResponse response = agent.executeAction(
    ActionRequest.of("forecast", Map.of("city", "Istanbul")));
```

The LLM does not receive a `forecast` function schema from this method. To hide an action from any LLM-facing list, set `excludeFromLLM = true`.

### 2. `@Tool` POJO — the model calls it

```java
public class WeatherTools {
    @Tool(name = "weather_forecast", description = "Forecast for a city")
    public String weatherForecast(
        @ToolParam(description = "City name, e.g. Istanbul") String city
    ) {
        return weatherService.forecast(city);
    }
}

Agent agent = AgentBuilder.create()
    .llm(llm)
    .role(role)
    .toolPojos(new WeatherTools())
    .principal(principal)
    .liabilitySink(sink)
    .authorityScope(scope)
    .build();
```

The dispatcher exposes `weather_forecast` as a function. Do not put API keys or other secrets in `@Tool` / `@ToolParam` descriptions or names — they are sent to the model. Keep credentials in environment variables (see [Custom Tools](../../capabilities/tools/custom-tools.md)). Per-call policy belongs on `setToolCallFilter` / before-hooks (TAN-2886), not in the schema.

### 3. `BuiltInTool` — shipped catalog, same dispatcher

There is no `BuiltInTool.HTTP_TOOLS`. For "let the model look this up" use a shipped search toolkit:

```java
Agent agent = AgentBuilder.create()
    .llm(llm)
    .role(role)
    .builtInTools(BuiltInTool.WEB_SEARCH_TOOLS)
    .principal(principal)
    .liabilitySink(sink)
    .authorityScope(scope)
    .build();
```

`WEB_SEARCH_TOOLS` instantiates `com.tnsai.tools.search.WebSearchTools` and registers its `@Tool` methods (`brave_search`, `duckduckgo`, …). Same `ToolMethodDispatcher` as a custom POJO.

## Do not

- Do not wrap a `BuiltInTool` POJO in an `@ActionSpec` just to "expose" it. Register the enum (or `toolPojos`) and let the dispatcher own the schema.
- Do not put an `@ActionSpec` on a method and expect the LLM to call it as a function. That is `@Tool`.
- Do not put secrets in tool schemas.
- Do not invent `BuiltInTool.HTTP_TOOLS` — it is not on the 0.13.0 enum.

## Related

- [Action System](action-system.md)
- [Custom Tools](../../capabilities/tools/custom-tools.md)
- [Tool Catalog](../../capabilities/tools/catalog.md)
- [Tool Registration](../../capabilities/tools/registration.md)
- [Annotation catalog](../../reference/annotations/catalog.md)
