Declarative workflow orchestration for Ash Framework.

Define multi-step workflows that combine human actions, background jobs, and time-based deadlines as a single DSL. Generates ash_state_machine states/transitions, ash_oban triggers, and Ash actions.

workflow

Define a workflow by declaring steps, transitions, and timeouts.

Nested DSLs

Options

NameTypeDefaultDocs
state_attributeatomThe attribute the workflow's current step is stored in. Defaults to state. AshWorkflow passes this down to ash_state_machine, so set it here rather than in the state_machine section. Use it when the resource already has a lifecycle column of its own, such as status.
scheduleranyThe module that decides when this workflow's automatic steps and timeouts run, optionally with its options: scheduler AshWorkflow.Scheduler.Oban scheduler {AshWorkflow.Scheduler.Oban, check_interval: "0 "} Defaults to the :scheduler application environment for :ash_workflow, and to AshWorkflow.Scheduler.Oban when that is unset. AshWorkflow.Scheduler.Oban requires the resource to also have the AshOban extension. AshWorkflow does not add it, so that a workflow using a scheduler unrelated to Oban does not carry the ash_oban DSL. See AshWorkflow.Scheduler for the behaviour an implementation satisfies.
queueatom:workflowThe Oban queue to use for all generated triggers. Defaults to :workflow.
check_intervalString.t"* * * * *"Oban cron expression controlling how often every generated trigger on this resource polls — both automatic steps and timeouts. Defaults to every minute. Each automatic step and each timeout gets its own scheduler, and every scheduler runs a query on every tick, so this multiplies: a resource with four automatic steps and four timeouts polls eight times a minute at the default. Raise it for workflows measured in days, and override individual timeouts with check_interval on the timeout itself.
generate_indexes?booleantrueWhether to add the composite indexes the generated triggers rely on. Only applies to resources using AshPostgres.DataLayer; other data layers ignore it. Every trigger filters on the state attribute, and every timeout also filters on its field, so without (state, field) indexes each poll is a sequential scan. Set to false if you manage these indexes yourself — a custom_indexes entry on the same fields already takes precedence. See AshWorkflow.Info.recommended_indexes/1.

workflow.step

step name

Declares a step in the workflow. Each step becomes a state in the generated state machine.

Nested DSLs

Arguments

NameTypeDefaultDocs
nameatomThe name of the step. Becomes a state in the generated state machine.

Options

NameTypeDefaultDocs
actionatomThe action to run for automatic steps. Must reference a user-defined update action on the resource.
initialbooleanfalseIf true, this step is the initial state. At most one step can be marked initial. If none are, the first non-terminal step by declaration order is used.
terminalbooleanfalseAsserts that this is an end state. A step that declares no action, no transitions, no timeouts, no on_success and no on_error is terminal whether or not this is set, so it is only needed to state the intent — the verifier then rejects the step if it grows an outgoing declaration.
on_erroratomThe step to transition to on failure. Optional, for automatic steps.
policyanyAn Ash policy check to apply to all transitions in this step. Accepts any {module, opts} tuple implementing Ash.Policy.Check.

workflow.step.transition

transition name

Declares a named transition from this manual step to another step.

Nested DSLs

Arguments

NameTypeDefaultDocs
nameatomThe name of the transition. Becomes an Ash update action.

Options

NameTypeDefaultDocs
toatomThe step to transition to. Omit when using conditional routes.
acceptlist(atom)[]List of resource attributes the generated transition action should accept as input.
undoable?booleanfalseIf true, this transition may be rewound by the generated undo action. Requires an undo block on the workflow. Opt-in per transition rather than per workflow, because undoing a decision that has already had effects outside the workflow — an offer sent, a payment taken — cannot be made safe by the extension. The default is that nothing is undoable. Note that undo restores state, not attributes: a transition with accept does not have its accepted values rolled back.

workflow.step.transition.route

route to

A conditional target for a transition. Evaluated at runtime.

Arguments

NameTypeDefaultDocs
toatomThe step to transition to if the condition matches.

Options

NameTypeDefaultDocs
whenanyAn Ash expression evaluated against the record with the action's changes applied, so it can read an attribute the same call accepted. Use expr(attribute == value).

Introspection

Target: AshWorkflow.Entities.Route

Introspection

Target: AshWorkflow.Entities.Transition

workflow.step.timeout

timeout name

Declares a time-based action or forced transition if the workflow stays in this step too long.

Nested DSLs

Arguments

NameTypeDefaultDocs
nameatomA unique name for this timeout.

Options

NameTypeDefaultDocs
fire_afteranyDuration tuple, e.g. {3, :days} or {2, :hours}.
fieldatom:state_entered_atThe datetime attribute or calculation to measure fire_after against. Defaults to :state_entered_at.
actionatomAction to run when the timeout fires. Does not change state.
transition_toatomStep to force-transition to when the timeout fires.
repeatbooleanfalseIf true, re-fire the timeout on the same interval.
self_scheduled?booleanfalseDeclares that you run this timeout's trigger yourself, more often than a cron expression can ask for. Cron cannot poll more often than once a minute, so a sub-minute fire_after is normally a compile error — the deadline would fire up to 60 seconds late. Setting this asserts that something else drives the trigger at the resolution the deadline needs, and permits the shorter duration. This changes nothing about what is generated: the scheduler module and its cron are still created, so AshOban.schedule/2 and AshOban.schedule_and_run_triggers/1 keep working. It only records the claim, and silences the check that would otherwise reject the duration.
check_intervalString.tOban cron expression for how often to check this timeout, overriding the workflow-level check_interval. Defaults to the workflow's setting, which itself defaults to every minute.

workflow.step.timeout.retry

Declares the failure policy for the step's or timeout's generated work: how many attempts and how long between them. Scheduler-neutral: AshWorkflow.Scheduler.Oban turns it into the trigger's max_attempts and backoff, and AshWorkflow.Scheduler.Precise re-arms its timer. Either way, on_error runs only after the final attempt fails.

Options

NameTypeDefaultDocs
max_attemptspos_integer1How many times the scheduler runs the step's action before it gives up. The default of 1 means one attempt and no retry, which is why a step declaring on_error moves to its error state on the first failure.
backoffany | :exponential:exponentialHow long to wait between attempts. A duration tuple such as {10, :seconds} is a fixed delay between attempts. :exponential grows the delay with the attempt number. Has no effect while max_attempts is 1.

Introspection

Target: AshWorkflow.Entities.Retry

Introspection

Target: AshWorkflow.Entities.Timeout

workflow.step.on_success

on_success to

Declares a step to transition to when this step's action succeeds, optionally guarded by a when condition. Repeatable: declare it more than once to fan out to different states depending on what the action computed. Entries are evaluated in declaration order, first match wins, against the record after the step's action has run. An entry with no when is unconditional and must be declared last, since it always matches and would otherwise shadow any entries after it.

Arguments

NameTypeDefaultDocs
toatomThe step to transition to.

Options

NameTypeDefaultDocs
whenanyAn Ash expression evaluated against the record after the action has run. Use expr(attribute == value). Omit for an unconditional route.

Introspection

Target: AshWorkflow.Entities.Route

workflow.step.retry

Declares the failure policy for the step's or timeout's generated work: how many attempts and how long between them. Scheduler-neutral: AshWorkflow.Scheduler.Oban turns it into the trigger's max_attempts and backoff, and AshWorkflow.Scheduler.Precise re-arms its timer. Either way, on_error runs only after the final attempt fails.

Options

NameTypeDefaultDocs
max_attemptspos_integer1How many times the scheduler runs the step's action before it gives up. The default of 1 means one attempt and no retry, which is why a step declaring on_error moves to its error state on the first failure.
backoffany | :exponential:exponentialHow long to wait between attempts. A duration tuple such as {10, :seconds} is a fixed delay between attempts. :exponential grows the delay with the attempt number. Has no effect while max_attempts is 1.

Introspection

Target: AshWorkflow.Entities.Retry

Introspection

Target: AshWorkflow.Entities.Step

workflow.transition_log

transition_log resource

Declares an opt-in transition log resource that records one row per workflow event. See AshWorkflow.Entities.TransitionLog.

Nested DSLs

Arguments

NameTypeDefaultDocs
resourceatomThe transition log resource module. Scaffolded with mix ash_workflow.gen.transition_log and validated at compile time by AshWorkflow.Verifiers.ValidateTransitionLog.

workflow.transition_log.belongs_to_actor

belongs_to_actor name, destination

Configures actor capture on the transition log.

Arguments

NameTypeDefaultDocs
nameatomThe attribute on the transition log resource that stores the actor, e.g. :user.
destinationatomThe actor resource module, e.g. MyApp.Accounts.User.

Introspection

Target: AshWorkflow.Entities.BelongsToActor

Introspection

Target: AshWorkflow.Entities.TransitionLog

workflow.undo

Enables undo for this workflow. Requires a transition_log, and at least one transition marked undoable?: true. See AshWorkflow.Entities.Undo.

Options

NameTypeDefaultDocs
withinanyHow long after a transition it may still be undone, e.g. {30, :minutes}. Measured against the undone row's occurred_at. Defaults to nil, which places no time limit on undo.
same_actor?booleanfalseIf true, only the actor recorded on a transition may undo it. Requires belongs_to_actor on the transition_log — without a recorded actor there is nothing to compare against, so this is rejected at compile time.
policyanyAn Ash policy check applied to the generated undo action. Accepts any {module, opts} tuple implementing Ash.Policy.Check. Step policies do not apply to undo: an undo spans two states, and which step it rewinds into is only known at runtime. Without this option the undo action falls under the extension's default-allow policy, like every other generated action.

Introspection

Target: AshWorkflow.Entities.Undo