Search
Search docs, blog posts, and ecosystem packages with citations.
Enter a query to see grounded citations.
We can't find the internet
Attempting to reconnect
Search docs, blog posts, and ecosystem packages with citations.
Build an end-to-end tool-calling agent that fetches weather data from external APIs.
Install the required dependencies and configure your LLM provider. The weather tools use the free National Weather Service API, so no weather API key is needed.
Mix.install([
{:jido, "~> 2.0"},
{:jido_ai, "~> 0.2"},
{:req, "~> 0.5"}
])
Configure your LLM provider. This example uses Anthropic, but any provider supported by jido_ai works.
Application.put_env(:jido_ai, :providers, [
anthropic: [api_key: System.fetch_env!("ANTHROPIC_API_KEY")]
])
Start the default Jido instance. This is idempotent, so calling it multiple times is safe.
{:ok, _} = Jido.start()
In Jido, tools are Actions. Any module that use Jido.Action can be exposed to an LLM as a callable tool. The LLM sees each action’s name, description, and schema, then decides when to invoke it.
Jido ships weather tools that wrap the free NWS (National Weather Service) API:
Jido.Tools.Weather - orchestrates a full weather lookup from coordinates Jido.Tools.Weather.Geocode - converts location strings to lat/lng via OpenStreetMap Nominatim Jido.Tools.Weather.Forecast - detailed period-by-period forecast from NWS Jido.Tools.Weather.CurrentConditions - latest conditions from NWS observation stations Jido.Tools.Weather.HourlyForecast - hour-by-hour planning data
The weather tools accept "lat,lng" coordinate strings, not city names. The geocode tool bridges the gap by converting human-readable locations to coordinates.
Define an agent module using use Jido.AI.Agent. This wires in the ReAct reasoning strategy, which handles the tool-calling loop automatically.
defmodule MyApp.WeatherAgent do
use Jido.AI.Agent,
name: "weather_agent",
description: "Weather assistant with tool access",
tools: [
Jido.Tools.Weather,
Jido.Tools.Weather.Geocode,
Jido.Tools.Weather.Forecast,
Jido.Tools.Weather.CurrentConditions
],
model: :fast,
max_iterations: 6,
system_prompt: """
You are a helpful weather assistant.
The weather tools accept "lat,lng" coordinates.
Use weather_geocode to convert city names to coordinates first.
Then fetch the forecast or current conditions.
Be conversational and provide practical advice.
"""
end
The key options control agent behavior:
tools - list of Jido.Action modules the LLM can call model: :fast - uses Claude Haiku for quick responses (alias defined by jido_ai) max_iterations - caps the number of ReAct reasoning loops before the agent stops system_prompt - guides the LLM on how and when to use each tool Start the agent under the default Jido instance, then send it a natural language query.
{:ok, pid} = Jido.start_agent(
Jido.default_instance(),
MyApp.WeatherAgent
)
Use ask_sync/3 for a blocking call that waits for the final answer:
{:ok, answer} = MyApp.WeatherAgent.ask_sync(
pid,
"What's the weather in Chicago?",
timeout: 60_000
)
IO.puts(answer)
For non-blocking usage, ask/3 returns a request handle you can await later:
{:ok, request} = MyApp.WeatherAgent.ask(
pid,
"Do I need an umbrella in Seattle?"
)
{:ok, answer} = MyApp.WeatherAgent.await(
request,
timeout: 60_000
)
The agent uses a ReAct (Reason + Act) loop to decide when tools are needed:
run/2 with the LLM-provided arguments max_iterations is reached For a weather query like “What’s the weather in Denver?”, the loop typically runs two iterations - one to geocode the city name, one to fetch the forecast.
You can inspect how Actions are converted to LLM tool definitions:
tools = Jido.AI.ToolAdapter.from_actions([
Jido.Tools.Weather,
Jido.Tools.Weather.Geocode
])
IO.inspect(hd(tools).name)
IO.inspect(hd(tools).parameter_schema)
ToolAdapter reads each Action’s name, description, and schema, then builds the JSON Schema that LLM providers require for structured tool calling.
Any Jido.Action module can serve as a tool. Define a schema with types and descriptions so the LLM knows what arguments to pass.
defmodule MyApp.TemperatureConverter do
use Jido.Action,
name: "convert_temperature",
description: "Convert between Fahrenheit and Celsius",
schema: [
value: [type: :float, required: true, doc: "Temperature value"],
from: [
type: {:in, [:fahrenheit, :celsius]},
required: true,
doc: "Source unit"
],
to: [
type: {:in, [:fahrenheit, :celsius]},
required: true,
doc: "Target unit"
]
]
@impl true
def run(%{value: v, from: :fahrenheit, to: :celsius}, _ctx) do
{:ok, %{result: Float.round((v - 32) * 5 / 9, 1), unit: "°C"}}
end
def run(%{value: v, from: :celsius, to: :fahrenheit}, _ctx) do
{:ok, %{result: Float.round(v * 9 / 5 + 32, 1), unit: "°F"}}
end
def run(%{value: v, from: same, to: same}, _ctx) do
unit = if same == :celsius, do: "°C", else: "°F"
{:ok, %{result: v, unit: unit}}
end
end
Add it to your agent by including it in the tools list alongside the weather tools.
Wrap common queries as functions on the agent module. This gives callers a typed API instead of raw string prompts.
defmodule MyApp.WeatherAgent do
use Jido.AI.Agent,
name: "weather_agent",
description: "Weather assistant with tool access",
tools: [
Jido.Tools.Weather,
Jido.Tools.Weather.Geocode,
Jido.Tools.Weather.Forecast,
Jido.Tools.Weather.CurrentConditions,
MyApp.TemperatureConverter
],
model: :fast,
max_iterations: 6,
system_prompt: """
You are a helpful weather assistant.
The weather tools accept "lat,lng" coordinates.
Use weather_geocode to convert city names to coordinates first.
Then fetch the forecast or current conditions.
Be conversational and provide practical advice.
"""
def get_forecast(pid, location, opts \\ []) do
ask_sync(pid, "What's the forecast for #{location}?",
Keyword.put_new(opts, :timeout, 60_000))
end
def need_umbrella?(pid, location, opts \\ []) do
ask_sync(pid, "Should I bring an umbrella in #{location} today?",
Keyword.put_new(opts, :timeout, 60_000))
end
end
These functions delegate to ask_sync/3 internally, so they return the same {:ok, answer} or {:error, reason} tuples.
ask_sync/3 returns {:error, reason} for provider timeouts, API failures, and max iteration exhaustion. Always pattern match on the result.
case MyApp.WeatherAgent.ask_sync(pid, query, timeout: 60_000) do
{:ok, answer} ->
IO.puts(answer)
{:error, reason} ->
IO.puts("Failed: #{inspect(reason)}")
end
Common failure modes include:
max_iterations without reaching a final answer The agent state remains intact after errors. You can retry the same query or ask a different question without restarting the agent process.
Now that you have a working tool-calling agent, explore these areas next.