# 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.