# Actions

An action is a role method annotated with `@ActionSpec`. Its `type` says who does the work: your Java code, an HTTP API, the model or an MCP server. `type` has no default, so every `@ActionSpec` names one.

## Action types

| `type` | What runs | The method body |
|--------|-----------|-----------------|
| `LOCAL` | The method itself, by reflection | Your code |
| `WEB_SERVICE` | An HTTP call described by `webService = @WebService(...)` | Skipped, unless it takes an `ActionResult` |
| `LLM` | One call to the agent's model | Skipped, unless it takes an `ActionResult` |
| `MCP_TOOL` | Tools on the MCP server in `mcpTool = @MCPTool(...)` | Skipped, unless it takes an `ActionResult` |

When the framework skips the body, write `throw Actions.dispatchedByFramework();` in it. A direct Java call then fails loudly instead of returning `null`.

## Declare actions

<!-- java-contract: src/main/java/com/example/tnsai/docs/ActionsExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.actions.ActionResult;
import com.tnsai.actions.Actions;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.ApprovalRequired;
import com.tnsai.annotations.Header;
import com.tnsai.annotations.MCPTool;
import com.tnsai.annotations.WebService;
import com.tnsai.enums.ActionType;
import com.tnsai.enums.AuthType;
import com.tnsai.enums.HttpMethod;
import com.tnsai.enums.role.ParamType;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;

import java.util.Map;

public final class ActionsExample {
    private ActionsExample() {}

    public static final class TravelRole extends Role {
        @Override
        public RoleIdentity getIdentity() {
            return new RoleIdentity("travel", "Plan trips", "travel");
        }

        // LOCAL: the framework calls this method.
        @ActionSpec(type = ActionType.LOCAL, description = "Count the nights between two dates")
        public long nights(String checkIn, String checkOut) {
            return java.time.temporal.ChronoUnit.DAYS.between(
                java.time.LocalDate.parse(checkIn), java.time.LocalDate.parse(checkOut));
        }

        // WEB_SERVICE without an ActionResult parameter: the body never runs,
        // and the HTTP response is the action's result.
        @ActionSpec(type = ActionType.WEB_SERVICE,
                    description = "Get the forecast for a city",
                    webService = @WebService(
                        endpoint = "https://api.example.com/v1/forecast",
                        method = HttpMethod.GET,
                        paramType = ParamType.PATH,
                        auth = AuthType.BEARER,
                        authTokenEnv = "WEATHER_API_KEY",
                        timeout = 5000,
                        headers = {@Header(key = "Accept", value = "application/json")}))
        public String forecast(String city) {
            throw Actions.dispatchedByFramework();
        }

        // WEB_SERVICE with an ActionResult parameter: the framework makes the call,
        // then runs this body with the response.
        @ActionSpec(type = ActionType.WEB_SERVICE,
                    description = "Get the current temperature for a city",
                    webService = @WebService(
                        endpoint = "https://api.example.com/v1/current",
                        method = HttpMethod.GET,
                        paramType = ParamType.QUERY))
        public String temperature(String city, ActionResult result) {
            Map<String, Object> body = result.asMap();
            return city + ": " + body.get("temp");
        }

        // LLM: the prompt is the description followed by the arguments.
        @ActionSpec(type = ActionType.LLM,
                    description = "Write a two-sentence packing tip for the trip",
                    llmTemperature = 0.3f)
        public String packingTip(String destination, String month) {
            throw Actions.dispatchedByFramework();
        }

        // MCP_TOOL: the agent offers this server's tools to the model.
        @ActionSpec(type = ActionType.MCP_TOOL,
                    description = "Currency tools",
                    mcpTool = @MCPTool(
                        serverUrl = "https://mcp.example.com/mcp",
                        apiKeyEnv = "FX_API_KEY"))
        public String currencyTools() {
            throw Actions.dispatchedByFramework();
        }

        // Blocked until the call carries an approved token.
        @ApprovalRequired(reason = "Books and charges a hotel")
        @ActionSpec(type = ActionType.LOCAL, description = "Book a hotel room")
        public String book(String hotelId, String checkIn) {
            return "booked " + hotelId + " from " + checkIn;
        }
    }
}
```

## Web service actions

`@WebService` describes one HTTP call.

- `endpoint` may hold `{name}` placeholders. Each one is replaced with the URL-encoded argument of that name.
- `paramType` then sends the arguments: `QUERY` appends them as a query string, `PATH` appends each value as a path segment, and `BODY` (the default) sends them as a JSON body. It applies to every argument, including any already used in a placeholder.
- `auth` is `NO_AUTH` (the default), `BEARER` or `BASIC`. `authTokenEnv` names the environment variable that holds the secret.
- `headers` adds fixed headers, and `timeout` is in milliseconds (default 30000).

## LLM actions

An `LLM` action makes one call to the agent's model. The prompt is the action's `description`, then any retrieved knowledge, then an `Input:` block with one `name: value` line per argument. The method body is not run to build the prompt.

- `llmSystemPrompt` sets the system message for this action only.
- `llmTemperature` sets the temperature when it is `0` or more. The default, `-1.0f`, keeps the client's own setting.

The call carries no tools, so the model cannot call tools from inside an `LLM` action. Tool calls happen in a chat turn; see [Actions and tools](actions-and-tools.md).

If the agent has no `LLMClient`, the action fails with `ActionExecutionException` in category `VALIDATION`. It does not return `null`, so a `@Fallback` method runs as it would for any other failure.

## MCP tool actions

An `MCP_TOOL` action connects the role to an MCP server. When the agent builds the tool list for a chat turn, it lists the server's tools with `tools/list` and offers each one the agent's authority allows. A tool call from the model then runs through this action. `apiKeyEnv` names the environment variable with the server's API key.

To connect MCP servers to the whole agent instead of one role, see [MCP client](../mcp/client.md).

## Post-process a result with `ActionResult`

Give a `WEB_SERVICE`, `LLM` or `MCP_TOOL` action an `ActionResult` parameter and the framework runs the body after the call, with the raw result in that parameter. The `temperature` action above uses it. If the body returns an `ActionResult`, the action's result is its value.

| Method | Returns |
|--------|---------|
| `getValue()` | The raw value |
| `asString()` | The value as a string |
| `asMap()` | The value parsed as a JSON object |
| `asList()` | The value parsed as a JSON array |
| `asJson()` | The value as a Jackson `JsonNode` |
| `as(Class<T>)` | The value converted to `T` |
| `isNull()`, `isEmpty()` | Checks on the raw value |

Without an `ActionResult` parameter, the action returns the raw result unchanged.

## Actions that need approval

`@ApprovalRequired` blocks an action until the call carries an approved `ApprovalToken` under the `_approval_token` parameter. The token must be bound to this agent and action, and it must not be expired or already used. Otherwise the call throws `ApprovalRequiredException`. See [Approvals](../security/approvals.md) for creating and approving tokens.

## What runs on every call

For each call, the framework runs these steps in order:

1. Check the agent's authority for the action, then any `@ApprovalRequired`.
2. Run the role's `@BeforeAction` hook and the input guardrail.
3. Validate the arguments against the action's parameters.
4. Check preconditions and invariants from `ActionContract` and `@Contract`. See [Contracts](contracts.md).
5. Retrieve the action's knowledge, if it has any.
6. Run the action inside its `@Resilience` retries and timeout. If it still fails, a matching `@Fallback` method runs. See [Resilience](resilience.md).
7. Run the output guardrail.
8. Check postconditions and invariants, then `@State` invariants when `@InvariantCheck` asks for them.
9. Run the role's `@AfterAction` hook.

A failure surfaces as `ActionExecutionException`. Its category, one of nine, says what went wrong and whether a retry can help. See [Exceptions](exceptions.md).

## Call an action from code

`agent.executeAction(name, parameters)` runs an action by name and returns its result. The `ActionRequest` overload returns an `ActionResponse`, and reports most failures in it instead of throwing. Cancellation and retrieval failures still throw.

```java
ActionResponse response = agent.executeAction(
    ActionRequest.of("nights", Map.of("checkIn", "2026-11-02", "checkOut", "2026-11-05")));

if (response.success()) {
    Object nights = response.value();
} else {
    log.warn("nights failed: {}", response.error());
}
```

Arguments are converted to the parameter types: strings to numbers, booleans and enums, and maps to records or POJOs.

## Related

- [Roles](roles.md): the classes that hold actions.
- [Actions and tools](actions-and-tools.md): when an action is the right choice.
- [Capabilities](capabilities.md): share body-less actions across roles.
- [Contracts](contracts.md): check an action's input and result.
- [Exceptions](exceptions.md): every error category.
