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
end

This generates an Ash policy targeting both :approve and :reject:

policies do
  policy action([:approve, :reject]) do
    authorize_if actor_attribute_equals(:role, :manager)
  end
end

Available 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
end

The 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