Skip to content

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.

  1. Get the tool call in onExecute.

    Call context.getToolCall() to retrieve an AiToolCall, the unique identifier linking the tool call to its async results. Forward the AiToolCall to every async operation you start.

    AiToolCall toolCall = context.getToolCall();
  2. Start an agent instance and run the action with withToolCall.

    Build and start an agent instance, then build AgentActionParameters with .withToolCall(toolCall) and .withResultHandler(MyToolActionAsyncHandler.class). Call agentService.runAgentAction(parameters) and retrieve the action execution ID from the returned AgentActionRunResult.

    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 __c agent actions from a tool.

  3. Declare async results and return an async response.

    Call context.newAsyncExecutionBuilder().withActionExecutionIds(VaultCollections.asList(actionExecutionId)).build() and return the result. withActionExecutionIds declares 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 calling onAsyncExecutionComplete. A status of NOT_COMPLETED keeps 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.

  4. Implement the result handler.

    Create a class that implements AgentActionResultHandler and is annotated with @AgentActionResultHandlerInfo. In onSuccess, call result.getToolCall(). A non-null value means the action was invoked from a tool.

    Build an AiToolResultRequest with the tool call, action execution ID, status, and any output values, then commit it using AiToolRuntimeService.appendAiToolResult(). In onError, follow the same pattern with AiToolResultStatus.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();
            }
        }
    }
  5. Implement onAsyncExecutionComplete.

    Override onAsyncExecutionComplete(AiToolTypeAsyncExecutionContext context) on the tool type class. Retrieve all async results via context.getAllAiToolAsyncResults(), then iterate and read each result. Return an AiToolTypeExecutionResponse.

    onAsyncExecutionComplete fires after every declared action execution has committed its result with a terminal status. A status of NOT_COMPLETED keeps the slot open.

    The following example shows both onExecute and onAsyncExecutionComplete in 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();
        }
    }

The 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 __c agent actions from a tool.
  • Parallel invocation of a Job and an agent action in the same tool call is not supported.