Actions

Discrete, composable units of validated computation that drive agent state transitions.

What actions solve

Agent systems need a consistent way to express “what should happen” without coupling logic to infrastructure. Raw functions lack validation, composability, and introspection. When you chain several operations together, you need guarantees about data shape at every boundary.

Actions solve this by wrapping each unit of work in a compile-time contract. Every Action declares its expected inputs, validates them at runtime, executes a pure function, and optionally validates the output. Because Actions never perform side effects directly, they remain testable in isolation and composable in sequence.

Anatomy of an action

An Action is an Elixir module that calls use Jido.Action with compile-time configuration. The only required callback is run/2, which receives validated params and a context map.

defmodule MyApp.CalculateShipping do
  use Jido.Action,
    name: "calculate_shipping",
    description: "Calculates shipping cost for an order",
    schema: Zoi.object(%{
      weight_kg: Zoi.float(),
      destination: Zoi.string()
    }),
    output_schema: Zoi.object(%{
      cost: Zoi.float(),
      carrier: Zoi.string()
    })

  @impl true
  def run(params, _context) do
    cost = params.weight_kg * rate_for(params.destination)
    {:ok, %{cost: cost, carrier: "standard"}}
  end

  defp rate_for("US"), do: 2.50
  defp rate_for(_), do: 5.00
end

The use macro validates your configuration at compile time. If you pass an invalid schema or omit the required name field, compilation fails with a clear error. This catches misconfiguration before your code ever runs.

The run/2 callback returns one of three shapes:

  • {:ok, result} - a map of output data
  • {:ok, result, directives} - output data plus side-effect instructions for the runtime
  • {:error, reason} - a failure

Validation

Actions use schema-based validation for both inputs and outputs. You declare schemas using Zoi in the schema and output_schema fields.

defmodule MyApp.CreateInvoice do
  use Jido.Action,
    name: "create_invoice",
    schema: Zoi.object(%{
      customer_id: Zoi.string(),
      line_items: Zoi.list(Zoi.map()),
      currency: Zoi.string() |> Zoi.default("USD")
    }),
    output_schema: Zoi.object(%{
      invoice_id: Zoi.string(),
      total: Zoi.float()
    })

  @impl true
  def run(params, _context) do
    total = Enum.reduce(params.line_items, 0.0, &(&1.amount + &2))
    {:ok, %{invoice_id: "inv_#{params.customer_id}", total: total}}
  end
end

Jido uses an open validation model. Only fields declared in the schema are validated - undeclared fields pass through untouched. This design is intentional. When actions compose in a chain, earlier actions may produce fields that later actions need. Open validation prevents intermediate actions from rejecting data they do not use.

You can also hook into the validation lifecycle. Override on_before_validate_params/1 to transform raw input before validation, or on_after_validate_params/1 to enrich validated params before execution.

Composition

Actions compose naturally because their inputs and outputs are plain maps. You pass a list of actions to cmd/2, and the framework executes them in sequence. Each action’s output merges into the params available to the next action.

defmodule MyApp.ValidateOrder do
  use Jido.Action,
    name: "validate_order",
    schema: Zoi.object(%{order_id: Zoi.string()})

  @impl true
  def run(params, _context) do
    {:ok, %{order_id: params.order_id, validated: true}}
  end
end

defmodule MyApp.ApplyDiscount do
  use Jido.Action,
    name: "apply_discount",
    schema: Zoi.object(%{validated: Zoi.boolean()})

  @impl true
  def run(params, _context) do
    discount = if params.validated, do: 0.10, else: 0.0
    {:ok, %{discount: discount}}
  end
end

Execute the chain through an agent:

{agent, directives} = MyAgent.cmd(agent, [
  {MyApp.ValidateOrder, %{order_id: "ord_99"}},
  MyApp.ApplyDiscount
])

ValidateOrder produces %{order_id: "ord_99", validated: true}. That map flows into ApplyDiscount, which finds the validated field it needs. Open validation makes this seamless - ApplyDiscount does not reject the order_id field even though its schema does not declare it.

Actions can also return directives as a third element to request side effects from the runtime:

def run(params, _context) do
  signal = %Jido.Signal{
    id: "sig_#{params.order_id}",
    type: "order.confirmed",
    source: "my_app.orders",
    data: %{order_id: params.order_id}
  }

  {:ok, %{status: :confirmed}, %Jido.Agent.Directive.Emit{signal: signal}}
end

The directive does not execute inline. It is collected and returned alongside the updated agent, keeping your action logic pure.

Instructions

An Instruction pairs an Action module with everything it needs to execute: parameters, context, and runtime options. When you call cmd/2, Jido normalizes whatever you pass into %Jido.Instruction{} structs before execution.

You can express instructions in several formats, from minimal to fully explicit:

Action module only

The simplest form. No params, no context.

MyAgent.cmd(agent, MyApp.ValidateOrder)

Tuple with params

The most common form. Pairs an action with its input data.

MyAgent.cmd(agent, {MyApp.ValidateOrder, %{order_id: "ord_99"}})

List of instructions

Chain multiple actions. Each format can be mixed freely in the list.

MyAgent.cmd(agent, [
  {MyApp.ValidateOrder, %{order_id: "ord_99"}},
  MyApp.ApplyDiscount,
  {MyApp.CalculateShipping, %{destination: "US"}}
])

Full struct

For cases where you need to set context or runtime options explicitly.

instruction = Jido.Instruction.new!(%{
  action: MyApp.ProcessOrder,
  params: %{order_id: "ord_99"},
  context: %{tenant_id: "tenant_456"},
  opts: [timeout: 10_000]
})

MyAgent.cmd(agent, instruction)

All formats normalize to the same %Jido.Instruction{} struct internally, so the execution path is identical regardless of which shorthand you use.

Actions as AI tools

Every Action doubles as an LLM tool definition. The same Zoi schema that validates runtime input also generates the JSON Schema that LLMs use to understand available parameters. You define the contract once, and both human callers and AI agents use the same interface.

Schema to JSON Schema

Jido.Action.Schema converts your Zoi schema into a JSON Schema object. The description option on each field becomes the parameter description the LLM sees.

defmodule MyApp.LookupCustomer do
  use Jido.Action,
    name: "lookup_customer",
    description: "Finds a customer by email address",
    schema: Zoi.object(%{
      email: Zoi.string(description: "The customer's email address")
    })

  @impl true
  def run(params, _context) do
    {:ok, %{customer_id: "cus_123", name: "Jane Doe"}}
  end
end

Call to_tool/0 on any Action to get a generic tool map with name, description, function, and parameters schema. This works with any tool-calling integration.

ReqLLM integration

When you use jido_ai, Jido.AI.ToolAdapter converts Actions into ReqLLM.Tool structs that plug directly into LLM API calls.

tools = Jido.AI.ToolAdapter.from_actions([
  MyApp.LookupCustomer,
  MyApp.CreateOrder
])

The adapter registers each tool with a noop callback. Jido does not execute tools inline during the LLM call. Instead, the agent runtime handles execution through Jido.AI.Directive.ToolExec, keeping the LLM request and the actual side effect cleanly separated.

Parameter normalization

LLMs return arguments as string-keyed JSON. Jido handles the mismatch automatically. When the runtime executes a tool call, convert_params_using_schema/2 converts string keys to atoms and coerces types based on your schema. A value like "42" for an integer field becomes 42. Unknown keys pass through untouched, preserving the open validation model.

Strict mode

Some providers (OpenAI structured outputs) require additionalProperties: false on every object in the schema. Pass strict: true to from_actions/2 or define a strict?/0 callback on your Action to opt in per-action. The adapter recursively enforces the constraint across nested objects.

Next steps

  • Agents - see how agents execute actions through cmd/2
  • Directives - understand the side-effect payloads actions can return
  • Signals - learn about typed event envelopes for agent coordination
  • Your first agent - hands-on tutorial using actions in practice