Tool Calling With Actions

Copy Markdown View Source

You want model-selected tool calls with Jido.Action modules, plus deterministic terminal shapes for one-shot and multi-turn runs.

After this guide, you can use:

Define A Tool Action

defmodule MyApp.Actions.Multiply do
  use Jido.Action,
    name: "multiply",
    schema: Zoi.object(%{a: Zoi.integer(), b: Zoi.integer()})

  @impl true
  def run(%{a: a, b: b}, _context), do: {:ok, %{product: a * b}}
end

Tool I/O Boundary

Tool actions may be pure or effectful. In ReAct-style loops, the model calls a tool because it needs the result before it can continue reasoning, so HTTP, LLM, database, file, or retrieval I/O inside run/2 is normal when that result is the tool output.

This direct I/O is separate from tool-emitted effects returned in the optional third tuple. effect_policy filters returned effects such as StateOp.SetState or Directive.Emit; it does not require every run/2 implementation to be pure.

Use directives, signals, or runtime integrations when the workflow has already decided on an outbound effect and wants the runtime to own delivery, retries, or observability.

Workflow stepUse
Fetch an account record, search an index, call a weather API, or ask another model so the current turn can continueEffectful tool action that returns the data from run/2
Send a final email, publish a webhook, emit an audit signal, or dispatch a notification after the decision is madeRuntime-owned directive, signal, or integration

One-Shot Tool Calling (CallWithTools)

One-shot mode returns a terminal turn map without executing tools automatically.

alias Jido.AI.Actions.ToolCalling.CallWithTools

params = %{
  prompt: "What is 6 * 7?",
  tools: ["multiply"]
}

context = %{
  tools: %{"multiply" => MyApp.Actions.Multiply}
}

{:ok, result} = Jido.Exec.run(CallWithTools, params, context)
# result.type == :tool_calls or :final_answer

Auto-Execute Tool Loop (CallWithTools)

Auto-execute mode runs tools and continues until a final answer or max-turn limit.

alias Jido.AI.Actions.ToolCalling.CallWithTools

params = %{
  prompt: "Use multiply to compute 6 * 7 and explain briefly.",
  tools: ["multiply"],
  auto_execute: true,
  max_turns: 5
}

context = %{
  tools: %{"multiply" => MyApp.Actions.Multiply}
}

{:ok, result} = Jido.Exec.run(CallWithTools, params, context)
# result.type == :final_answer when loop completes
# result.turns includes executed loop turns
# result.messages includes assistant/tool conversation messages

Deterministic terminal shapes:

  • completed loop: %{type: :final_answer, text: text, usage: usage, turns: turns, messages: messages, model: model}
  • max turns reached: %{type: :tool_calls, reason: :max_turns_reached, turns: max_turns, usage: usage, model: model}

Direct Tool Execution (ExecuteTool)

Use direct execution when your app already chose the tool and args.

alias Jido.AI.Actions.ToolCalling.ExecuteTool

params = %{
  tool_name: "multiply",
  params: %{a: 6, b: 7}
}

context = %{
  tools: %{"multiply" => MyApp.Actions.Multiply}
}

{:ok, result} = Jido.Exec.run(ExecuteTool, params, context)
# %{tool_name: "multiply", status: :success, result: %{product: 42}}

Tool Discovery And Security Filtering (ListTools)

ListTools defaults to excluding sensitive tool names (include_sensitive: false) and returns only public metadata (name, optional serialized schema).

alias Jido.AI.Actions.ToolCalling.ListTools

context = %{
  tools: %{
    "multiply" => MyApp.Actions.Multiply,
    "admin_delete_user" => MyApp.Actions.AdminDeleteUser
  }
}

{:ok, public_tools} = Jido.Exec.run(ListTools, %{}, context)
{:ok, all_tools} = Jido.Exec.run(ListTools, %{include_sensitive: true}, context)
{:ok, allowlisted} = Jido.Exec.run(ListTools, %{allowed_tools: ["multiply"]}, context)

Security filtering behavior:

  • default denylist filtering by sensitive name fragments (admin, delete, token, secret, etc.)
  • explicit override with include_sensitive: true
  • optional allowlist hard filter with allowed_tools: [...]

Tool Registry Precedence (Tool Map / Context / Plugin State)

CallWithTools, ExecuteTool, and ListTools resolve tool registries with this precedence:

  1. context[:tools]
  2. context[:tool_calling][:tools]
  3. context[:chat][:tools]
  4. context[:state][:tool_calling][:tools]
  5. context[:state][:chat][:tools]
  6. context[:agent][:state][:tool_calling][:tools]
  7. context[:agent][:state][:chat][:tools]
  8. context[:plugin_state][:tool_calling][:tools]
  9. context[:plugin_state][:chat][:tools]

First non-nil registry wins. This keeps behavior deterministic across direct action execution, plugin-routed calls, and fallback context paths.

Dynamic Registration On A Running Agent

{:ok, _agent} = Jido.AI.register_tool(agent_pid, MyApp.Actions.Multiply)
{:ok, true} = Jido.AI.has_tool?(agent_pid, "multiply")

Transform Agent Tool Calls And Results

Jido.AI.Agent provides optional callbacks for data that must have a different model-facing representation. For example, an agent can replace a long record key with a short alias in a tool result, then resolve that alias before the model calls another tool.

defmodule MyApp.SupportAgent do
  use Jido.AI.Agent,
    name: "support_agent",
    tools: [MyApp.Actions.GetTicket, MyApp.Actions.UpdateTicket]

  @impl Jido.AI.ToolInterceptor
  def before_tool_call(%{arguments: arguments} = tool_call, context) do
    resolved_arguments = MyApp.ToolAliases.resolve(arguments, context)
    {:ok, %{tool_call | arguments: resolved_arguments}}
  end

  @impl Jido.AI.ToolInterceptor
  def after_tool_call(_tool_call, {:ok, result, effects}, context) do
    projected_result = MyApp.ToolAliases.project(result, context)
    {:ok, {:ok, projected_result, effects}}
  end

  def after_tool_call(_tool_call, result, _context), do: {:ok, result}
end

The callback contract is:

  • before_tool_call/2 runs once before argument validation and retries.
  • It can change arguments, but it cannot change the call ID, tool name, or action module.
  • after_tool_call/3 runs once after the final attempt.
  • It receives and must return a canonical {:ok | :error, value, effects} result.

  • The active effect policy filters effects returned by after_tool_call/3.
  • Callback errors and invalid returns stop the current tool round.

Callbacks receive the configured tool context plus runtime data such as :state, :request_id, and :agent_id. Keep callback work short. The callbacks run in the agent strategy path, not in the action process.

These callbacks apply to agent-managed ReAct and Tree-of-Thoughts tool execution. Direct Jido.Exec, Jido.AI.Turn, CallWithTools, and ExecuteTool calls do not invoke them.

Failure Mode: Tool Execution Returns :not_found

Symptom:

  • ExecuteTool returns tool-not-found error content

Fix:

  • pass a tools map in one of the registry precedence paths above
  • verify module.name/0 matches the requested tool_name

Defaults You Should Know

  • CallWithTools auto_execute default: false
  • CallWithTools max_turns default: 10 (hard-capped by validation)
  • ExecuteTool timeout default: 30_000ms
  • ListTools schema inclusion default: true
  • ListTools sensitive filtering default: enabled (include_sensitive: false)

Next