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
.
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.
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
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!
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
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 theinstance_of
constraint, like so:action :get, :struct do constraints instance_of: __MODULE__ run # ... end
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.
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
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")