<!--
SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors>
SPDX-License-Identifier: MIT
-->
# Generic Actions
Generic 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`.
```elixir
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! {: .tip}
>
> Generic actions can omit a return type, in which case running them returns `:ok` if successful.
>
> ```elixir
> 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](dsl-ash-resource.html#actions-action).
## Calling Generic Actions
The basic formula for calling a generic action looks like this:
```elixir
Resource
|> Ash.ActionInput.for_action(:action_name, %{argument: :value}, ...opts)
|> Ash.run_action!()
```
See the [code interface guide](/documentation/topics/code-interfaces.md) 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
```elixir
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` and `ash_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:
```elixir
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 {: .info}
>
> 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:
>
> ```elixir
> 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.
>
> ```elixir
> 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:
1. **Prepare the action input:** Use `Ash.ActionInput.for_action/4` to specify the resource, the action and its arguments.
2. **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`:
```elixir
action :say_hello, :string do
argument :name, :string, allow_nil?: false
run fn input, _ ->
{:ok, "Hello: #{input.arguments.name}"}
end
end
```
Call this action:
```elixir
{: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](documentation/topics/resources/code-interfaces.md) to call actions:
Given a definition like:
```elixir
define :say_hello, args: [:name]
```
```elixir
{: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.
```elixir
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:
```elixir
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.
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
1. Global preparations/validations (in order of definition)
2. Action preparations/validations (in order of definition)
3. `before_action` hooks
4. Action logic execution
5. `after_action` hooks (success only)
This order ensures that global business logic runs first, followed by action-specific logic, and finally the action hooks.