Actions
View SourcePrerequisites: Getting Started • Your Second Action
Actions are the core building blocks of Jido Action - self-contained modules that define validated, observable operations with clear input/output contracts.
Action Anatomy
Every action has the same basic structure:
defmodule MyApp.Actions.Example do
use Jido.Action,
# Required metadata
name: "example_action",
description: "What this action does",
# Parameter validation
schema: [
input: [type: :string, required: true],
options: [type: :map, default: %{}]
]
# Required: core execution logic
@impl true
def run(params, context) do
{:ok, %{result: "processed"}}
end
endSchema Definition
Actions support two schema formats: NimbleOptions and Zoi (recommended for new code).
NimbleOptions Schema
schema: [
# Basic types
name: [type: :string, required: true],
age: [type: :integer, min: 0, max: 150],
active: [type: :boolean, default: true],
# Collections
tags: [type: {:list, :string}, default: []],
config: [type: :map, default: %{}],
# Custom validation
email: [
type: :string,
required: true,
doc: "User's email address"
],
# Enums and choices
status: [
type: :atom,
in: [:pending, :active, :inactive],
default: :pending
]
]Zoi Schema (Recommended)
Zoi schemas provide richer validation with built-in transformations:
use Jido.Action,
name: "create_user",
schema: Zoi.object(%{
email: Zoi.string() |> Zoi.trim() |> Zoi.email(),
age: Zoi.integer() |> Zoi.min(0) |> Zoi.max(120),
name: Zoi.string() |> Zoi.min_length(1) |> Zoi.max_length(100),
role: Zoi.enum([:admin, :user, :guest]) |> Zoi.default(:user)
})Zoi advantages:
- Built-in transformations (trim, downcase, coerce)
- Rich refinements for custom validation
- Better error messages
- Type coercion out of the box
Schema Features
Type Safety: Validates parameter types at runtime
Required Fields: Ensures critical parameters are provided
Default Values: Sensible defaults for optional parameters
Constraints: Min/max values, string patterns, list lengths
Documentation: Built-in parameter documentation for AI tools
Lifecycle Hooks
Extend action behavior with optional lifecycle callbacks:
defmodule MyApp.Actions.ProcessData do
use Jido.Action,
name: "process_data",
schema: [data: [type: :string, required: true]]
# 1. Pre-validation hook (before schema validation)
@impl true
def on_before_validate_params(params) do
# Normalize or enrich parameters before validation
normalized = Map.update(params, :data, "", &String.trim/1)
{:ok, normalized}
end
# 2. Post-validation hook (after schema validation)
@impl true
def on_after_validate_params(params) do
# Further processing after validation
{:ok, params}
end
# 3. Main execution (required)
@impl true
def run(params, context) do
processed = expensive_operation(params.data)
{:ok, %{result: processed, processed_at: DateTime.utc_now()}}
end
# 4. Post-execution hook - receives {:ok, result} or {:error, reason}
@impl true
def on_after_run({:ok, result}) do
# Log, cache, or enrich the result
Logger.info("Data processed successfully")
{:ok, Map.put(result, :logged, true)}
end
def on_after_run({:error, _} = error), do: error
# 5. Error compensation (called when compensation is enabled)
@impl true
def on_error(failed_params, error, context, opts) do
# Clean up resources, send alerts, etc.
cleanup_temp_files(failed_params.data)
{:ok, %{cleanup_performed: true}}
end
endAvailable Lifecycle Hooks
| Hook | Called | Purpose |
|---|---|---|
on_before_validate_params/1 | Before schema validation | Normalize/enrich input params |
on_after_validate_params/1 | After schema validation | Post-process validated params |
on_before_validate_output/1 | Before output validation | Pre-process action output |
on_after_validate_output/1 | After output validation | Post-process validated output |
on_after_run/1 | After action execution | Receives {:ok, result} or {:error, reason} |
on_error/4 | On error (when compensation enabled) | Cleanup/rollback on failure |
Hook Execution Order
Parameters → on_before_validate_params → Schema Validation → on_after_validate_params → run
↓
Result ← on_after_validate_output ← Output Validation ← on_before_validate_output ← on_after_run
↓
Error → on_errorCompensation & Error Recovery
Enable compensation for critical operations:
defmodule MyApp.Actions.CreateOrder do
use Jido.Action,
name: "create_order",
compensation: [enabled: true, max_retries: 3],
schema: [
user_id: [type: :string, required: true],
items: [type: {:list, :map}, required: true]
]
def run(params, context) do
with {:ok, user} <- validate_user(params.user_id),
{:ok, order} <- create_order_record(params.items),
{:ok, _} <- charge_payment(order) do
{:ok, order}
end
end
# Compensation runs on error
def on_error(failed_params, error, context, opts) do
case error.type do
:execution_error ->
# Clean up any partial state
cancel_pending_order(failed_params.user_id)
refund_payment_if_charged(failed_params.user_id)
{:ok, %{compensated: true}}
_ ->
{:ok, %{compensated: false}}
end
end
endOutput Validation
Validate action outputs for consistency:
defmodule MyApp.Actions.GenerateReport do
use Jido.Action,
name: "generate_report",
schema: [type: [type: :atom, in: [:summary, :detailed]]],
# Validate output structure
output_schema: [
title: [type: :string, required: true],
content: [type: :string, required: true],
generated_at: [type: :string, required: true],
metadata: [type: :map, default: %{}]
]
def run(params, _context) do
{:ok, %{
title: "#{params.type} Report",
content: generate_content(params.type),
generated_at: DateTime.utc_now() |> DateTime.to_iso8601(),
metadata: %{type: params.type}
}}
end
endAI Tool Integration
Actions automatically convert to AI-compatible tool definitions:
# Get tool definition
tool_def = MyApp.Actions.ProcessData.to_tool()
# Returns LangChain-compatible tool definition:
%{
name: "process_data",
description: "Processes input data",
function: #Function<...>, # Executable function
parameters_schema: %{
"type" => "object",
"properties" => %{
"data" => %{"type" => "string", "description" => "Input data"}
},
"required" => ["data"]
}
}
# Execute from AI tool call (handles string keys and type conversion)
{:ok, result} = Jido.Action.Tool.execute_action(
MyApp.Actions.ProcessData,
%{"data" => "input from AI"},
%{}
)Advanced Patterns
Context-Aware Actions
def run(params, context) do
# Access execution context
user_id = context[:user_id]
request_id = context[:request_id]
# Use context for business logic
result = process_for_user(params, user_id)
{:ok, Map.put(result, :request_id, request_id)}
endDynamic Configuration
defmodule MyApp.Actions.ConfigurableProcessor do
use Jido.Action,
name: "configurable_processor",
schema: [
data: [type: :string, required: true],
# Configuration from application environment
timeout: [type: :integer, default: Application.get_env(:my_app, :default_timeout, 5000)]
]
endResource Management
def run(params, _context) do
# Acquire resources
{:ok, connection} = Database.connect()
try do
result = Database.query(connection, params.query)
{:ok, result}
after
# Always clean up
Database.disconnect(connection)
end
endBest Practices
Design Principles
- Single Responsibility: One action = one operation
- Clear Contracts: Use comprehensive schemas
- Idempotency: Safe to retry when possible
- Context Isolation: Use context for cross-cutting concerns only
Performance
- Lazy Loading: Load resources only when needed
- Timeouts: Set reasonable timeouts for external calls
- Resource Pooling: Reuse expensive resources like DB connections
- Telemetry: Instrument critical paths
Error Handling
- Structured Errors: Use
Jido.Action.Errorhelpers - Meaningful Messages: Provide actionable error messages
- Graceful Degradation: Handle partial failures elegantly
- Compensation: Implement cleanup for critical operations
Next Steps
→ Schemas & Validation - Deep dive into NimbleOptions and Zoi schemas
→ Execution Engine - Robust action execution
→ Instructions & Plans - Workflow composition
→ Error Handling Guide - Advanced error patterns