AshWorkflow integrates with Ash's policy system to control who can trigger workflow transitions.
Every policy in this guide needs Ash.Policy.Authorizer on the resource. AshWorkflow.Transformers.AddPolicies generates nothing without it, so a policy on a step used to be accepted and then never enforced. AshWorkflow.Verifiers.ValidateStepPolicies now rejects that at compile time.
use Ash.Resource,
domain: MyApp.Documents,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshWorkflow, AshOban]Step-level policies
Add a policy to a manual step to restrict all its transitions to a specific kind of actor:
step :manager_review do
policy actor_attribute_equals(:role, :manager)
transition :approve, to: :approved
transition :reject, to: :rejected
endThis generates an Ash policy targeting both :approve and :reject:
policies do
policy action([:approve, :reject]) do
authorize_if actor_attribute_equals(:role, :manager)
end
endAvailable checks
The policy field accepts any {module, opts} tuple implementing Ash.Policy.Check. Common checks available inside the step block:
policy actor_attribute_equals(:role, :admin)
policy relates_to_actor_via(:assigned_to)
policy actor_present()For checks not imported by default, pass the tuple directly:
policy {MyApp.Checks.BelongsToTeam, []}Default allow-all policy
When any step has a policy declaration, the extension generates a default "allow all" policy at the end of the policy list. This ensures workflow actions without explicit policies, like create actions and automatic step actions, aren't blocked.
Resource-level policies
For complex authorization — multi-condition rules, relationship checks, or custom modules — define policies directly on the resource:
policies do
policy action(:approve) do
authorize_if relates_to_actor_via(:assigned_reviewer)
authorize_if actor_attribute_equals(:role, :admin)
end
endThe extension skips generating step-level policies for actions that already have user-defined policies targeting them.
Enabling the authorizer
Step-level policies only take effect if the resource has Ash.Policy.Authorizer configured:
use Ash.Resource,
domain: MyApp.Domain,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshWorkflow, AshOban]Without the authorizer, policies are defined but not enforced.
Querying permitted actions
The generated :available_actions calculation is authorization-aware. When loaded with an actor, it returns only the actions that actor is permitted to perform:
# Without actor — returns all transitions for the current step
doc = Ash.load!(doc, :available_actions)
doc.available_actions
#=> [:approve, :reject]
# With actor — filtered by policies
doc = Ash.load!(doc, :available_actions, actor: %{role: :viewer})
doc.available_actions
#=> []
doc = Ash.load!(doc, :available_actions, actor: %{role: :manager})
doc.available_actions
#=> [:approve, :reject]It calls Ash.can?/2 for each action to check it against the actor's permissions.
SAT solver dependency
Ash's policy authorizer requires a SAT solver at compile time. Add one of these to your mix.exs:
{:picosat_elixir, "~> 0.2"} # recommended for production (NIF)
{:simple_sat, "~> 0.1"} # pure Elixir alternative