Skip to content
tnsaiDocs
GitHub

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

typeWhat runsThe method body
LOCALThe method itself, by reflectionYour code
WEB_SERVICEAn HTTP call described by webService = @WebService(...)Skipped, unless it takes an ActionResult
LLMOne call to the agent's modelSkipped, unless it takes an ActionResult
MCP_TOOLTools 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

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.

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.

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.

MethodReturns
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 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.
  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.
  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.

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.

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.