Execute tools outside of the agent’s control for enhanced security and flexibility.
External tool execution gives you complete control over when and how certain tools actually run. Instead of letting the agent execute the tool directly, it pauses and waits for you to handle the execution yourself. This is incredibly useful when you need:
Enhanced security: Execute sensitive operations in a controlled environment
External service calls: Integrate with services that require special handling
Database operations: Run queries through your own connection management
Custom execution logic: Add validation, logging, or rate limiting before execution
When you mark a tool with @tool(external_execution=True), your agent will:
Pause execution when the tool is about to be called
Set is_paused to True on the run response
Populate tools_awaiting_external_execution with tools that need external handling
Wait for you to execute the tool and set its result
Continue execution once you call continue_run() with the result
The key difference from other HITL patterns is that the agent never actually calls the function—you’re responsible for the entire execution.
import subprocessfrom agno.agent import Agentfrom agno.models.openai import OpenAIResponsesfrom agno.tools import toolfrom agno.utils import pprint# Create a tool with the correct name, arguments and docstring for the agent to know what to call.@tool(external_execution=True)def execute_shell_command(command: str) -> str: """Execute a shell command. Args: command (str): The shell command to execute Returns: str: The output of the shell command """ return subprocess.check_output(command, shell=True).decode("utf-8")agent = Agent( model=OpenAIResponses(id="gpt-5.2"), tools=[execute_shell_command], markdown=True,)run_response = agent.run("What files do I have in my current directory?")for requirement in run_response.active_requirements: if requirement.is_external_tool_execution: if requirement.tool_execution.tool_name == execute_shell_command.name: print(f"Executing {requirement.tool_execution.tool_name} with args {requirement.tool_execution.tool_args} externally") # Execute the tool manually. You can execute any function or process here and use the tool_args as input. result = execute_shell_command.entrypoint(**requirement.tool_execution.tool_args) # Set the result on the tool execution object so that the agent can continue requirement.external_execution_result = resultrun_response = agent.continue_run(run_id=run_response.run_id, requirements=run_response.requirements)pprint.pprint_run_response(run_response)
In this example, the agent identifies that it needs to run execute_shell_command but doesn’t actually execute it. Instead, it pauses and gives you the tool name and arguments. You then execute it yourself (or something completely different!) and provide the result back.
When a run is paused for external execution, the returned RunOutput will contain a list of requirement objects.These requirement objects will contain the tool executions that need to run outside of the agent’s run.You can find the tool related to each requirement in requirement.tool_execution. Each tool execution object contains:
tool_name: The name of the tool that was called
tool_args: A dictionary of arguments the agent wants to pass to the tool
external_execution_required: A boolean flag set to True
result: Where you set the execution result (initially None)
You can iterate through these requirements, execute the tools however you want, and set their results:
for requirement in run_response.active_requirements: if requirement.is_external_tool_execution: print(f"Tool: {requirement.tool_execution.tool_name}") print(f"Args: {requirement.tool_execution.tool_args}") # Execute your custom logic here result = my_custom_execution(requirement.tool_execution.tool_args) # Set the result so the agent can continue requirement.external_execution_result = result# After resolving the requirement, you can continue the run:response = agent.continue_run(run_id=run_response.run_id, requirements=run_response.requirements)
Important: You must resolve all external tool execution requirements before calling continue_run().
An external tool execution requirement is considered resolved when you set requirement.external_execution_result.Else Agno will raise a ValueError, letting you know that not all requirements have been resolved.
If you’re using a Toolkit, you can specify which tools require external execution using the external_execution_required_tools parameter:
from agno.tools.toolkit import Toolkitimport subprocessclass ShellTools(Toolkit): def __init__(self, *args, **kwargs): super().__init__( tools=[self.list_dir, self.get_env], external_execution_required_tools=["list_dir"], # Only this one needs external execution *args, **kwargs, ) def list_dir(self, directory: str): """Lists the contents of a directory.""" return subprocess.check_output(f"ls {directory}", shell=True).decode("utf-8") def get_env(self, var_name: str): """Gets an environment variable.""" import os return os.getenv(var_name, "Not found")agent = Agent( model=OpenAIResponses(id="gpt-5.2"), tools=[ShellTools()], markdown=True,)run_response = agent.run("What files are in my current directory and what's my PATH?")for requirement in run_response.active_requirements: if requirement.is_external_tool_execution: # Only list_dir will be here, get_env runs normally if requirement.tool_execution.tool_name == "list_dir": result = ShellTools().list_dir(**requirement.tool_execution.tool_args) requirement.external_execution_result = result# After resolving the requirement, you can continue the run:response = agent.continue_run(run_id=run_response.run_id, requirements=run_response.requirements)
This lets you mix external and internal tools in the same toolkit—perfect when you only need special handling for specific operations.
You can absolutely have a mix of regular tools and external execution tools in the same agent.When the agent wants to call multiple tools, only the ones marked with @tool(external_execution=True) will cause a pause:
@tool(external_execution=True)def sensitive_database_query(query: str) -> str: """Execute a database query.""" pass@tooldef safe_calculation(x: int, y: int) -> int: """Perform a safe calculation.""" return x + yagent = Agent( model=OpenAIResponses(id="gpt-5.2"), tools=[sensitive_database_query, safe_calculation], markdown=True,)response = agent.run("Calculate 5 + 10 and query the users table")# Agent will pause when it tries to call sensitive_database_query# but safe_calculation executes normallyfor requirement in response.active_requirements: if requirement.is_external_tool_execution: if requirement.tool_execution.tool_name == "sensitive_database_query": # Execute with your own DB connection and security checks result = execute_safe_db_query(requirement.tool_execution.tool_args["query"]) requirement.external_execution_result = result# After resolving the requirement, you can continue the run:response = agent.continue_run(run_id=response.run_id, requirements=response.requirements)
You can also use external execution with streaming responses:
for run_event in agent.run("What files are in my directory?", stream=True): if run_event.is_paused: for requirement in run_event.active_requirements: if requirement.is_external_tool_execution: # Execute externally result = execute_tool_externally(requirement.tool_execution.tool_args) requirement.external_execution_result = result # Continue streaming for response in agent.continue_run( run_id=run_event.run_id, requirements=run_event.requirements, stream=True ): print(response.content, end="") else: print(run_event.content, end="")
Always set results: Make sure you set requirement.external_execution_result for all requirements before continuing
Error handling: Wrap your external execution in try-catch blocks and provide meaningful error messages as results
Security validation: Use external execution to add extra security checks before running sensitive operations
Logging: Log all external executions for audit trails
Timeouts: Consider adding timeouts to your external execution logic to prevent hanging
Remember that external execution tools marked with @tool(external_execution=True) are mutually exclusive with @tool(requires_confirmation=True) and @tool(requires_user_input=True).A tool can only use one of these patterns at a time.