AshWorkflow.Entities.Every (AshWorkflow v0.7.1)

Copy Markdown View Source

Defines a recurring action entity with its configuration schema.

every is not a timeout: nothing is timing out. It is a recurring action that runs on an interval for as long as a record sits in its step. Firing never leaves the step — there is no transition_to — so every always requires an action.

Storage

AshWorkflow.Transformers.AddAttributes adds one nilable :utc_datetime_usec attribute per every, holding the instant it last fired. Its interval is measured against that column, not against state_entered_at, so two every entities on the same step — and any timeout sharing the step — no longer share one anchor that one firing resets out from under the others.

Named <step>_<every>_last_fired_at by default, or explicitly with last_fired_field:

every :reminder do
  interval {1, :hours}
  action :send_reminder
  last_fired_field :reminder_last_fired_at
end

Not field — AshWorkflow.Entities.Timeout's field names an anchor AshWorkflow reads and never writes. last_fired_field names a column AshWorkflow owns and writes on every fire. Reusing the name would give it two opposite meanings.

A record whose column is still nil has never fired this every, and the interval is then measured from state_entered_at, so the first firing lands one whole interval after the record entered the step rather than on entry. Both the generated Oban trigger and AshWorkflow.Scheduler.Precise.Timeline apply that fallback.

Bounding an every with until

every alone fires forever. until stops it after a fixed amount of wall-clock time since the record entered the step:

every :reminder do
  interval {2, :days}
  action :send_review_reminder
  until {8, :days}
end

until is measured against state_entered_at directly. An every's own firing writes its last_fired_field, not state_entered_at, so state_entered_at stays put for as long as the record occupies the step — nothing moves the anchor until measures against. See AshWorkflow.Verifiers.ValidateEvery for why until must be strictly longer than interval.

Reaching the bound only stops the firing. It does not transition state — compose a second, ordinary timeout with a fire_after equal to the bound for "give up and move on".

Summary

Functions

The attribute this every writes its last-fired instant to, and measures interval against: last_fired_field if given, otherwise <step_name>_<every_name>_last_fired_at.

Types

duration_unit()

@type duration_unit() :: AshWorkflow.Duration.unit()

t()

@type t() :: %AshWorkflow.Entities.Every{
  __spark_metadata__: term(),
  action: atom(),
  check_interval: String.t() | nil,
  interval: {pos_integer(), duration_unit()},
  last_fired_field: atom() | nil,
  name: atom(),
  retry: AshWorkflow.Entities.Retry.t() | nil,
  self_scheduled?: boolean(),
  until: AshWorkflow.Duration.t() | nil
}

Functions

attribute_schema()

last_fired_field(step_name, every)

@spec last_fired_field(atom(), t()) :: atom()

The attribute this every writes its last-fired instant to, and measures interval against: last_fired_field if given, otherwise <step_name>_<every_name>_last_fired_at.

validate_duration(value)