How to Invoke an Agent Action from a Custom Tool Type
A custom tool type can invoke one or more agent actions asynchronously. In onExecute, the tool starts the agent actions, returns a pending async response, and the platform calls onAsyncExecutionComplete after all declared results are committed.
Before following these steps, create a custom tool type.
-
Get the tool call in
onExecute.Call
context.getToolCall()to retrieve anAiToolCall, the unique identifier linking the tool call to its async results. Forward theAiToolCallto every async operation you start.AiToolCall toolCall = context.getToolCall(); -
Start an agent instance and run the action with
withToolCall.Build and start an agent instance, then build
AgentActionParameterswith.withToolCall(toolCall)and.withResultHandler(MyToolActionAsyncHandler.class). CallagentService.runAgentAction(parameters)and retrieve the action execution ID from the returnedAgentActionRunResult.StartAgentInstanceResult agentInstanceResult = agentService.startAgentInstance( agentService.newStartAgentInstanceRequestBuilder() .withAgentConfigurationName("classroom_agent__c") .withScopeSource(scopeSource) .build()); String agentInstanceId = agentInstanceResult.getAgentInstanceId(); AgentActionParameters parameters = agentService.newAgentActionParametersBuilder() .withAgentInstanceId(agentInstanceId) .withActionName("summarize_course__c") .withToolCall(toolCall) .withResultHandler(MyToolActionAsyncHandler.class) .build(); AgentActionRunResult runResult = agentService.runAgentAction(parameters); String actionExecutionId = runResult.getActionInstanceId();Developers can only invoke
__cagent actions from a tool. -
Declare async results and return an async response.
Call
context.newAsyncExecutionBuilder().withActionExecutionIds(VaultCollections.asList(actionExecutionId)).build()and return the result.withActionExecutionIdsdeclares the action executions the platform must wait for. The platform waits until every declared action execution has committed its result with a terminal status before callingonAsyncExecutionComplete. A status ofNOT_COMPLETEDkeeps the slot open.return context.newAsyncExecutionBuilder() .withActionExecutionIds(VaultCollections.asList(actionExecutionId)) .build();The maximum number of action execution IDs per tool call is five. Each action execution ID must be unique within the tool call.
-
Implement the result handler.
Create a class that implements
AgentActionResultHandlerand is annotated with@AgentActionResultHandlerInfo. InonSuccess, callresult.getToolCall(). A non-null value means the action was invoked from a tool.Build an
AiToolResultRequestwith the tool call, action execution ID, status, and any output values, then commit it usingAiToolRuntimeService.appendAiToolResult(). InonError, follow the same pattern withAiToolResultStatus.FAILURE.package com.veeva.vault.custom.actionhandler; import … @AgentActionResultHandlerInfo public class MyToolActionAsyncHandler implements AgentActionResultHandler { @Override public void onSuccess(AgentActionSuccess result) { AiToolRuntimeService toolRuntimeService = ServiceLocator.locate(AiToolRuntimeService.class); LogService logService = ServiceLocator.locate(LogService.class); AiToolCall toolCall = result.getToolCall(); if (toolCall != null) { JsonObject output = result.getOutput(0, AgentActionOutputType.JSON); toolRuntimeService.appendAiToolResult( toolRuntimeService.newAiToolResultRequestBuilder() .withToolCall(toolCall) .withActionExecutionId(result.getActionInstanceId()) .withCompleteStatus(AiToolResultStatus.SUCCESS) .withValue("output", output) .build()) .onSuccess(response -> logService.info("Result {} committed as {}", response.getActionExecutionId(), response.getStatus())) .onError(error -> logService.error("Commit failed for {}: {}", error.getActionExecutionId(), error.getErrorMessage())) .execute(); } } @Override public void onError(AgentActionError error) { AiToolRuntimeService toolRuntimeService = ServiceLocator.locate(AiToolRuntimeService.class); LogService logService = ServiceLocator.locate(LogService.class); AiToolCall toolCall = error.getToolCall(); if (toolCall != null) { toolRuntimeService.appendAiToolResult( toolRuntimeService.newAiToolResultRequestBuilder() .withToolCall(toolCall) .withActionExecutionId(error.getActionInstanceId()) .withCompleteStatus(AiToolResultStatus.FAILURE) .withValue("errorMessage", error.getMessage()) .build()) .onSuccess(response -> logService.info("Failure result {} committed as {}", response.getActionExecutionId(), response.getStatus())) .onError(appendError -> logService.error("Commit failed for {}: {}", appendError.getActionExecutionId(), appendError.getErrorMessage())) .execute(); } } } -
Implement
onAsyncExecutionComplete.Override
onAsyncExecutionComplete(AiToolTypeAsyncExecutionContext context)on the tool type class. Retrieve all async results viacontext.getAllAiToolAsyncResults(), then iterate and read each result. Return anAiToolTypeExecutionResponse.onAsyncExecutionCompletefires after every declared action execution has committed its result with a terminal status. A status ofNOT_COMPLETEDkeeps the slot open.The following example shows both
onExecuteandonAsyncExecutionCompletein a complete tool type implementation.package com.veeva.vault.custom.tooltype; import … @ExecuteAs(ExecuteAsUser.REQUEST_OWNER) @AiToolTypeRuntimeHandlerInfo(dynamicToolSpec = false) public class InvokeAgentToolType implements AiToolTypeRuntimeHandler { @Override public AiToolTypeExecutionResponse onExecute(AiToolTypeExecuteContext context) { AgentService agentService = ServiceLocator.locate(AgentService.class); AiService aiService = ServiceLocator.locate(AiService.class); JsonObject toolInput = context.getInput(); String courseId = toolInput.getValue("course_id", JsonValueType.STRING); AiScopeSource scopeSource = aiService.newAiObjectScopeSourceBuilder() .withObjectName("course__c") .withRecordId(courseId) .build(); StartAgentInstanceResult agentInstanceResult = agentService.startAgentInstance( agentService.newStartAgentInstanceRequestBuilder() .withAgentConfigurationName("classroom_agent__c") .withScopeSource(scopeSource) .build()); AiToolCall toolCall = context.getToolCall(); String agentInstanceId = agentInstanceResult.getAgentInstanceId(); AgentActionParameters parameters = agentService.newAgentActionParametersBuilder() .withAgentInstanceId(agentInstanceId) .withActionName("summarize_course__c") .withToolCall(toolCall) .withResultHandler(MyToolActionAsyncHandler.class) .build(); AgentActionRunResult runResult = agentService.runAgentAction(parameters); return context.newAsyncExecutionBuilder() .withActionExecutionIds(VaultCollections.asList(runResult.getActionInstanceId())) .build(); } @Override public AiToolTypeExecutionResponse onAsyncExecutionComplete(AiToolTypeAsyncExecutionContext context) { List<AiToolResult> results = context.getAllAiToolAsyncResults(); AiToolResult result = results.get(0); JsonObject output = result.getValue("output", AgentActionParametersValueType.OBJECT); return context.newSuccessBuilder(AiToolTypeExecutionResponse.JsonResultBuilder.class) .withJson(output) .build(); } }
Constraints
Section link for ConstraintsThe following limits and restrictions apply when invoking agent actions from a custom tool type:
- The maximum number of action execution IDs per tool call is five.
- Each action execution ID must be unique within the tool call. Duplicate declarations are retry-safe.
- Developers may only invoke
__cagent actions from a tool. - Parallel invocation of a Job and an agent action in the same tool call is not supported.