AshWorkflow.Info (AshWorkflow v0.8.0)

Copy Markdown View Source

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 the every entities declared on a given step, or [] if the step has none or does not exist.

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 step the transition transition_name would move record to, without running it.

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

merged_transition()

@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

available_actions(resource, step_name)

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

everys(resource, step_name)

Returns the every entities declared on a given step, or [] if the step has none or does not exist.

in_terminal_state?(record)

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

initial_step(resource)

@spec initial_step(Ash.Resource.t()) :: AshWorkflow.Entities.Step.t() | nil

Returns the initial step for the workflow.

scheduled_work(resource)

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

scheduler(resource)

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

state_attribute(resource)

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

step(resource, step_name)

@spec step(Ash.Resource.t(), atom()) :: AshWorkflow.Entities.Step.t() | nil

Returns a single workflow step by name, or nil if not found.

steps(resource)

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

terminal?(resource, step_name)

@spec terminal?(Ash.Resource.t(), atom()) :: boolean()

Returns true if the given step is terminal (an end state with no outgoing transitions).

transition(resource, name)

@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
#=> }

transition_log(resource)

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

transition_target(record, transition_name, input \\ %{})

@spec transition_target(Ash.Resource.record(), atom(), map()) ::
  {:ok, atom() | nil} | {:error, term()}

Returns the step the transition transition_name would move record to, without running it.

Resolves routes the same way the generated transition action does (AshWorkflow.Changes.ConditionalTransition): in declaration order, first match wins, scoped to the step the record is in. A static transition such as transition :reject, to: :rejected resolves to its to.

Returns:

  • {:ok, step} — the step the transition would land in
  • {:ok, nil} — the transition would not move the record: the record's current step does not declare it, or no route matches. The action itself fails in both cases
  • {:error, error} — a route's when failed to evaluate. The action fails on the same error

Raises ArgumentError when no step declares transition_name.

Routes are evaluated against record as given, and this function loads nothing. A when that reads an unloaded relationship or calculation sees nil. The generated action does the same with the record it is called on, so load what the routes read before previewing or running the transition. The :transition_targets calculation loads it for you.

A route that reads accepted input sees that input only when the transition runs. To preview a particular call, pass its input as input. Only the keys the transition accepts are applied, the same as the action applies them. Keys are attribute names as atoms, and values are used as given, not cast.

Example

AshWorkflow.Info.transition_target(candidate, :advance)
#=> {:ok, :compliance}

AshWorkflow.Info.transition_target(document, :decide, %{decision: :approve})
#=> {:ok, :approved}

undo(resource)

@spec undo(Ash.Resource.t() | map()) :: AshWorkflow.Entities.Undo.t() | nil

Returns the workflow's undo configuration, or nil if undo is not enabled.

undoable_edge?(resource, from_state, to_state)

@spec undoable_edge?(Ash.Resource.t() | map(), atom(), atom()) :: boolean()

Returns true if the forward move from_state -> to_state may be rewound.

undoable_edges(resource)

@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}]

workflow_graph(resource)

@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, nil for every other step
  • :policy — the step's policy check, or nil
  • :retry — the step's AshWorkflow.Entities.Retry, or nil
  • :initial — true for the step the workflow starts in, as AshWorkflow.Entities.Step.find_initial/1 picks it
  • :terminal — true for an end state
  • :manual — true when nothing runs on entry
  • :wait_state — true when a timeout is the step's only exit

The edges are five 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's when expression as :condition; a simple transition contributes one entry with a nil condition. :undoable? is true only when the workflow declares an undo block and the transition opts in.
  • :on_success — one entry per declared on_success route, as %{to:, condition:}.
  • :on_error — the step an automatic step falls to on failure, or nil.
  • :timeouts — one entry per timeout, as %{name:, to:, fire_after:, fire_at:, field:, action:, retry:}. A timeout that runs an action rather than moving the workflow has a nil :to and a non-nil :action. A fire_at timeout names the field holding its deadline, so it has a nil :fire_after and a nil :field.
  • :everys — one entry per every, as %{name:, interval:, action:, until:, retry:}. Always measures against state_entered_at and never has a target, since firing never leaves the step. :until is nil unless the every bounds its firing.

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: :breach, to: :escalated, fire_after: {1, :hours}, fire_at: nil, field: :state_entered_at, action: nil, retry: nil},
#=>       %{name: :expire, to: :expired, fire_after: nil, fire_at: :offer_expires_at, field: nil, action: nil, retry: nil}
#=>     ],
#=>     everys: [
#=>       %{name: :chase, interval: {3, :days}, action: :send_reminder, until: nil, retry: nil}
#=>     ]
#=>   },
#=>   ...
#=> }