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
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.
endpointmay hold{name}placeholders. Each one is replaced with the URL-encoded argument of that name.paramTypethen sends the arguments:QUERYappends them as a query string,PATHappends each value as a path segment, andBODY(the default) sends them as a JSON body. It applies to every argument, including any already used in a placeholder.authisNO_AUTH(the default),BEARERorBASIC.authTokenEnvnames the environment variable that holds the secret.headersadds fixed headers, andtimeoutis 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.
llmSystemPromptsets the system message for this action only.llmTemperaturesets the temperature when it is0or 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.
| 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 for creating and approving tokens.
What runs on every call
For each call, the framework runs these steps in order:
- Check the agent's authority for the action, then any
@ApprovalRequired. - Run the role's
@BeforeActionhook and the input guardrail. - Validate the arguments against the action's parameters.
- Check preconditions and invariants from
ActionContractand@Contract. See Contracts. - Retrieve the action's knowledge, if it has any.
- Run the action inside its
@Resilienceretries and timeout. If it still fails, a matching@Fallbackmethod runs. See Resilience. - Run the output guardrail.
- Check postconditions and invariants, then
@Stateinvariants when@InvariantCheckasks for them. - Run the role's
@AfterActionhook.
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.
Related
- Roles: the classes that hold actions.
- Actions and tools: when an action is the right choice.
- Capabilities: share body-less actions across roles.
- Contracts: check an action's input and result.
- Exceptions: every error category.