Introspection helpers for AshWorkflow resources.
Summary
Types
The merged view of a transition name, as returned by transition/2.
Functions
Returns the list of user-facing action names available at a given step.
Returns true if the given record is in a terminal state.
Returns the initial step for the workflow.
Returns the composite indexes that make the generated Oban triggers cheap, as a list of attribute-name lists, most useful first.
Returns every AshWorkflow.Scheduler.Work the workflow declares.
Returns the workflow's scheduler as {module, options}.
Returns the attribute the workflow stores its current step in.
Returns a single workflow step by name, or nil if not found.
Returns all workflow step entities for a resource.
Returns true if the given step is terminal (an end state with no outgoing transitions).
Returns the merged view of a transition name, or nil if no step declares it.
Returns the workflow's transition_log configuration, or nil if no
transition log is configured.
Returns the workflow's undo configuration, or nil if undo is not enabled.
Returns true if the forward move from_state -> to_state may be rewound.
Returns the set of state changes that may be rewound, as {from_state, to_state} tuples describing the forward move.
Returns a graph representation of the workflow as a map.
Types
@type merged_transition() :: %{ name: atom(), from: [atom()], routes: [%{from: atom(), to: atom(), when: Ash.Expr.t() | nil}], accepted_inputs: [atom()], generated_action: atom() }
The merged view of a transition name, as returned by transition/2.
Functions
@spec available_actions(Ash.Resource.t(), atom()) :: [atom()]
Returns the list of user-facing action names available at a given step.
For manual steps, returns the transition names. For automatic and terminal steps, returns an empty list.
@spec in_terminal_state?(Ash.Resource.record()) :: boolean()
Returns true if the given record is in a terminal state.
The record must have its state attribute loaded.
@spec initial_step(Ash.Resource.t()) :: AshWorkflow.Entities.Step.t() | nil
Returns the initial step for the workflow.
@spec recommended_indexes(Ash.Resource.t() | map()) :: [[atom()]]
Returns the composite indexes that make the generated Oban triggers cheap, as a list of attribute-name lists, most useful first.
Every trigger's where clause filters on the state attribute, and every
timeout also filters on its field. Because ago/2 compiles to a bind
parameter rather than a per-row function call, a timeout's filter reaches the
data layer as state = $1 AND state_entered_at <= $2 — an ordinary composite
range scan. Without these indexes each poll is a sequential scan.
A [state, field] index also serves the automatic-step triggers, which
filter on the state attribute alone, since it is the leading column. The
state attribute is returned on its own when a workflow declares no timeouts
at all. A workflow that renames the attribute with state_attribute gets
indexes on that name instead.
Calculation-backed timeout fields are omitted: they are not columns, so they cannot be indexed directly.
For resources using AshPostgres.DataLayer these are added automatically as
custom_indexes — see AshWorkflow.Transformers.AddIndexes. This function is
for everyone else, and for tooling.
Example
AshWorkflow.Info.recommended_indexes(MyApp.CandidatePipeline)
#=> [[:state, :state_entered_at], [:state, :last_session_date]]
@spec scheduled_work(Ash.Resource.t() | map()) :: [AshWorkflow.Scheduler.Work.t()]
Returns every AshWorkflow.Scheduler.Work the workflow declares.
One per automatic step and one per timeout, as
AshWorkflow.Transformers.AddScheduler built them and handed them to the
selected scheduler. A runtime scheduler reads this rather than re-deriving
the list from steps and timeouts, so both see exactly the same work.
@spec scheduler(Ash.Resource.t() | map()) :: {module(), keyword()}
Returns the workflow's scheduler as {module, options}.
Falls back to the :scheduler application environment for :ash_workflow,
and then to AshWorkflow.Scheduler.Oban.
@spec state_attribute(Ash.Resource.t() | map()) :: atom()
Returns the attribute the workflow stores its current step in.
Defaults to :state. A workflow overrides it with state_attribute on the
workflow section, which is passed down to ash_state_machine.
@spec step(Ash.Resource.t(), atom()) :: AshWorkflow.Entities.Step.t() | nil
Returns a single workflow step by name, or nil if not found.
@spec steps(Ash.Resource.t() | map()) :: [AshWorkflow.Entities.Step.t()]
Returns all workflow step entities for a resource.
Accepts either a compiled resource module or an in-progress DSL state, so transformers can share the same introspection.
@spec terminal?(Ash.Resource.t(), atom()) :: boolean()
Returns true if the given step is terminal (an end state with no outgoing transitions).
@spec transition(Ash.Resource.t() | map(), atom()) :: merged_transition() | nil
Returns the merged view of a transition name, or nil if no step declares it.
A transition name declared on more than one step becomes a single generated
action, and the declarations merge: from lists every step the action moves
out of, routes holds one entry per declared target with the when
expression that selects it, and accepted_inputs is the union of every
declaration's accept.
A route from a static transition has a when of nil. The step it leaves is
on the route itself, so a caller reading a route never has to pair it back up
with a step.
Example
AshWorkflow.Info.transition(MyApp.OnboardingWorkflow, :complete)
#=> %{
#=> name: :complete,
#=> from: [:initial_review, :detailed_review],
#=> routes: [
#=> %{from: :initial_review, to: :detailed_review, when: nil},
#=> %{from: :detailed_review, to: :approved, when: nil}
#=> ],
#=> accepted_inputs: [:notes],
#=> generated_action: :complete
#=> }
@spec transition_log(Ash.Resource.t() | map()) :: AshWorkflow.Entities.TransitionLog.t() | nil
Returns the workflow's transition_log configuration, or nil if no
transition log is configured.
@spec undo(Ash.Resource.t() | map()) :: AshWorkflow.Entities.Undo.t() | nil
Returns the workflow's undo configuration, or nil if undo is not enabled.
@spec undoable_edge?(Ash.Resource.t() | map(), atom(), atom()) :: boolean()
Returns true if the forward move from_state -> to_state may be rewound.
@spec undoable_edges(Ash.Resource.t() | map()) :: [{atom(), atom()}]
Returns the set of state changes that may be rewound, as {from_state, to_state} tuples describing the forward move.
A conditional transition contributes one edge per route, so undo permits exactly the moves the transition could actually have made. Returns an empty list when undo is not enabled.
Example
AshWorkflow.Info.undoable_edges(MyApp.OnboardingWorkflow)
#=> [{:review, :approved}, {:review, :rejected}]
@spec workflow_graph(Ash.Resource.t()) :: %{required(atom()) => map()}
Returns a graph representation of the workflow as a map.
Each key is a step name, and the value describes that step and every edge leaving it, so a caller can draw and label the whole workflow without reaching back into the DSL.
The step itself carries:
:name— the step name, repeated so an entry stands alone once taken out of the map:action— the action an automatic step runs,nilfor every other step:policy— the step'spolicycheck, ornil:retry— the step'sAshWorkflow.Entities.Retry, ornil:initial—truefor the step the workflow starts in, asAshWorkflow.Entities.Step.find_initial/1picks it:terminal—truefor an end state:manual—truewhen nothing runs on entry:wait_state—truewhen a timeout is the step's only exit
The edges are four lists:
:transitions— one entry per reachable target, as%{name:, to:, condition:, undoable?:, accept:}. A conditional transition contributes one entry per route, each carrying that route'swhenexpression as:condition; a simple transition contributes one entry with anilcondition.:undoable?istrueonly when the workflow declares anundoblock and the transition opts in.:on_success— one entry per declaredon_successroute, as%{to:, condition:}.:on_error— the step an automatic step falls to on failure, ornil.:timeouts— one entry per timeout, as%{name:, to:, fire_after:, field:, action:, repeat:, retry:}. A timeout that runs an action rather than moving the workflow has anil:toand a non-nil:action.
Example
AshWorkflow.Info.workflow_graph(MyApp.OnboardingWorkflow)
#=> %{
#=> screening: %{
#=> name: :screening,
#=> action: nil,
#=> policy: nil,
#=> retry: nil,
#=> initial: true,
#=> terminal: false,
#=> manual: true,
#=> wait_state: false,
#=> transitions: [
#=> %{name: :advance, to: :interviewing, condition: nil, undoable?: true, accept: [:notes]},
#=> %{name: :reject, to: :rejected, condition: nil, undoable?: false, accept: []}
#=> ],
#=> on_success: [],
#=> on_error: nil,
#=> timeouts: [
#=> %{name: :chase, to: nil, fire_after: {3, :days}, field: :state_entered_at, action: :send_reminder, repeat: true, retry: nil}
#=> ]
#=> },
#=> ...
#=> }