Generic Actions
View SourceGeneric actions are so named because there are no special rules about how they work. A generic action takes arguments and returns a value. The struct used for building input for a generic action is Ash.ActionInput
.
action :say_hello, :string do
argument :name, :string, allow_nil?: false
run fn input, _ ->
{:ok, "Hello: #{input.arguments.name}"}
end
end
A generic action declares its arguments, return type, and implementation, as illustrated above.
No return? No problem!
Generic actions can omit a return type, in which case running them returns :ok
if successful.
action :schedule_job do
argument :job_name, :string, allow_nil?: false
run fn input, _ ->
# Schedule the job
:ok
end
end
For a full list of all of the available options for configuring generic actions, see the Ash.Resource.Dsl documentation.
Calling Generic Actions
The basic formula for calling a generic action looks like this:
Resource
|> Ash.ActionInput.for_action(:action_name, %{argument: :value}, ...opts)
|> Ash.run_action!()
See the code interface guide guide for how to define idiomatic and convenient functions that call your actions.
Why use generic actions?
The example above could be written as a normal function in elixir, i.e
def say_hello(name), do: "Hello: #{name}"
The benefit of using generic actions instead of defining normal functions:
- They can be used with api extensions like
ash_json_api
andash_graphql
- Their inputs are type checked and casted
- They support Ash authorization patterns (i.e policies)
- They can be included in the code interface of a resource
- They can be made transactional with a single option (
transaction? true
)
If you don't need any of the above, then there is no problem with writing regular Elixir functions!
Return types and constraints
Generic actions do not cast their return types. It is expected that the action return a valid value for the type that they declare. However, declaring additional constraints can inform API usage, and make the action more clear. For example:
action :priority, :integer do
constraints [min: 1, max: 3]
argument :status, :atom, constraints: [one_of: [:high, :medium, :low]]
run fn input, _ ->
case input.arguments.status do
:high -> {:ok, 3}
:medium -> {:ok, 2}
:low -> {:ok, 1}
end
end
end
Returning resource instances
It sometimes happens that you want to make a generic action which returns an
instance or instances of the resource. It's natural to assume that you can
set your action's return type to the name of your resource. This won't work
as resources do not define a type, unless they are embedded. In embedded resources, this won't work because the module is still being compiled, so referencing yourself as a type causes a compile error. Instead, use the :struct
type and the instance_of
constraint, like so:
action :get, :struct do
constraints instance_of: __MODULE__
run # ...
end
For returning many instances of the resource, you can set your action's return type to
{:array, :struct}
and set the items
constraint to the name of your resource.
action :list_resources, {:array, :struct} do
constraints items: [instance_of: __MODULE__]
run # ...
end
Calling Generic Actions
To execute a generic action in Ash, follow these steps:
- Prepare the action input: Use
Ash.ActionInput.for_action/4
to specify the resource, the action and its arguments. - Run the action: Use
Ash.run_action/2
to execute the action with the prepared input.
Example Usage
Consider an Ash.Resource
with the action :say_hello
:
action :say_hello, :string do
argument :name, :string, allow_nil?: false
run fn input, _ ->
{:ok, "Hello: #{input.arguments.name}"}
end
end
Call this action:
{:ok, greeting} = Resource
|> Ash.ActionInput.for_action(:say_hello, %{name: "Alice"})
|> Ash.run_action()
IO.puts(greeting) # Output: Hello: Alice
Using Code Interface
You can also use Code Interfaces to call actions:
Given a definition like:
define :say_hello, args: [:name]
{:ok, greeting} = Resource.say_hello("Alice")
greeting = Resource.say_hello!("Alice")
Validations and Preparations
Generic actions support validations and preparations, allowing you to add business logic and input validation to your actions.
Validations
Validations in generic actions work similarly to those in other action types. They validate the action input before the action logic runs.
action :create_user, :struct do
constraints instance_of: __MODULE__
argument :name, :string, allow_nil?: false
argument :email, :string, allow_nil?: false
argument :age, :integer
validate present([:name, :email])
validate match(:email, ~r/@/)
validate compare(:age, greater_than: 13) do
message "Must be at least 13 years old"
end
run fn input, _ ->
# Create user logic here
{:ok, %__MODULE__{
name: input.arguments.name,
email: input.arguments.email,
age: input.arguments.age
}}
end
end
You can also use custom validation modules:
action :transfer_funds, :boolean do
argument :from_account, :string, allow_nil?: false
argument :to_account, :string, allow_nil?: false
argument :amount, :decimal, allow_nil?: false
validate {MyApp.Validations.SufficientFunds, field: :amount}
run fn input, _ ->
# Transfer logic here
{:ok, true}
end
end
Preparations
Preparations allow you to modify the action input before the action runs. This is useful for setting computed values or applying business logic.
action :audit_log, :string do
argument :action, :string, allow_nil?: false
argument :details, :map, default: %{}
prepare fn input, _context ->
# Add timestamp and actor information
updated_details = Map.merge(input.arguments.details, %{
timestamp: DateTime.utc_now(),
actor_id: input.context[:actor]&.id
})
Ash.ActionInput.set_argument(input, :details, updated_details)
end
run fn input, _ ->
# Log the action
log_entry = "#{input.arguments.action}: #{inspect(input.arguments.details)}"
{:ok, log_entry}
end
end
You can also use the built-in build
preparation:
action :search_with_defaults do
argument :query, :string
argument :filters, :map, default: %{}
prepare build(
arguments: %{
filters: expr(Map.merge(^arg(:filters), %{active: true}))
}
)
run fn input, _ ->
# Search logic with default filters applied
{:ok, perform_search(input.arguments.query, input.arguments.filters)}
end
end
Action Hooks
Generic actions support action-level hooks that run before and after the action execution.
Before Action Hooks
Before action hooks run immediately before the action logic executes:
action :process_payment, :boolean do
argument :amount, :decimal, allow_nil?: false
argument :payment_method, :string, allow_nil?: false
validate present([:amount, :payment_method])
# Using a function
prepare before_action(fn input, _context ->
# Log the payment attempt
Logger.info("Processing payment of #{input.arguments.amount}")
# Validate payment method
if input.arguments.payment_method not in ["credit_card", "bank_transfer"] do
Ash.ActionInput.add_error(input, "Invalid payment method")
else
input
end
end)
run fn input, _ ->
# Process payment logic
{:ok, true}
end
end
After Action Hooks
After action hooks run after successful action execution:
action :send_notification, :boolean do
argument :message, :string, allow_nil?: false
argument :recipient, :string, allow_nil?: false
prepare after_action(fn input, result, _context ->
# Log successful notification
Logger.info("Notification sent to #{input.arguments.recipient}")
# Could perform additional side effects here
{:ok, result}
end)
run fn input, _ ->
# Send notification logic
send_notification(input.arguments.recipient, input.arguments.message)
{:ok, true}
end
end
Using Custom Preparation Modules
You can also create reusable preparation modules for generic actions:
defmodule MyApp.Preparations.AuditAction do
use Ash.Resource.Preparation
def prepare(input, _opts, context) do
Ash.ActionInput.before_action(input, fn input ->
# Log the action attempt
MyApp.AuditLog.log_action(input.action.name, input.arguments, context.actor)
input
end)
|> Ash.ActionInput.after_action(fn input, result ->
# Log successful completion
MyApp.AuditLog.log_success(input.action.name, result, context.actor)
{:ok, result}
end)
end
end
Then use it in your action:
action :sensitive_operation, :boolean do
argument :data, :map, allow_nil?: false
prepare MyApp.Preparations.AuditAction
run fn input, _ ->
# Sensitive operation logic
{:ok, true}
end
end
Global Validations and Preparations
Generic actions also support global validations and preparations defined at the resource level:
defmodule MyApp.MyResource do
use Ash.Resource
# Global preparations that apply to all actions
preparations do
prepare fn input, _context ->
# Add tenant information to all actions
Ash.ActionInput.set_context(input, %{tenant: "default"})
end do
# Only apply to generic actions
on: [:action]
end
end
# Global validations that apply to all actions
validations do
validate present(:actor) do
message "Authentication required"
on: [:action] # Only apply to generic actions
end
end
actions do
action :my_action do
# Action-specific logic
end
end
end
Execution Order
For generic actions, the execution order is:
- Global preparations/validations (in order of definition)
- Action preparations/validations (in order of definition)
before_action
hooks- Action logic execution
after_action
hooks (success only)
This order ensures that global business logic runs first, followed by action-specific logic, and finally the action hooks.