A macro-based DSL for defining pgflow workflows.
This module provides a declarative way to define workflow steps with dependencies,
retries, timeouts, and array processing capabilities. Use it by calling use PgFlow.Flow
in your flow module.
Example
defmodule MyApp.Flows.Example do
use PgFlow.Flow
@flow queue: :example, max_attempts: 3, base_delay: 5, timeout: 60
step :first do
fn input, ctx ->
%{result: input["value"] * 2}
end
end
step :second, depends_on: [:first] do
fn deps, ctx ->
%{doubled: deps.first["result"]}
end
end
map :process_items, array: :second do
fn item, ctx ->
%{processed: item}
end
end
endFlow Options
The @flow module attribute accepts the following options:
:queue- (required) atom identifier for the flow queue (also accepts:slugas alias):max_attempts- maximum retry attempts for failed steps (default: 1):base_delay- base delay in seconds for exponential backoff (default: 1):timeout- step execution timeout in seconds (default: 30):cron- (optional) schedule this flow via pg_cron with sub-options::schedule- (required) cron schedule string (e.g., "@hourly", "0 9 *"):input- (optional) static input map passed to each scheduled run
Step Options
Steps defined with step/2 or step/3 accept these options:
:depends_on- list of step atoms this step depends on:handler- module implementing PgFlow.StepHandler (alternative to block):max_attempts- override flow-level max_attempts:base_delay- override flow-level base_delay:timeout- override flow-level timeout:start_delay- seconds to delay before starting this step:if- map pattern that must match for the step to run (optional):if_not- map pattern that must not match for the step to run (optional):when_unmet- what happens when:ifor:if_notis not satisfied::fail,:skip, or:skip_cascade(default:skipwhen a condition is set):when_exhausted- what happens when retries are exhausted::fail,:skip, or:skip_cascade(default:fail)
Conditional Step Execution
Steps can be made conditional using :if or :if_not options with PostgreSQL @> (contains)
pattern matching on JSON objects:
Root Steps
For root steps (those with no dependencies), the pattern is matched against the flow's input map:
step :process, if: %{"mode" => "active"} do
fn input, _ctx ->
# Only runs if input contains {"mode" => "active"}
%{processed: true}
end
endDependent Steps
For steps with dependencies, the pattern is matched against a map built from the
dependencies' outputs: %{dep_slug => output}:
step :charge, depends_on: [:validate] do
fn deps, _ctx ->
%{charged: true}
end
end
step :send_receipt, depends_on: [:charge], if: %{"charge" => %{"success" => true}} do
fn deps, _ctx ->
# Only runs if deps.charge contains {"success" => true}
%{sent: true}
end
endCondition Behavior
:when_unmet- Action when condition is not satisfied (requires:ifor:if_not; default:skip)::skip- Skip this step; its key is omitted from dependent step inputs:skip_cascade- Skip this step and all steps downstream of it (transitively):fail- Fail this step (bypassing handler execution)
:when_exhausted- Action when retries exhaust (default:fail)::skip- Skip after max retries:skip_cascade- Skip after max retries and cascade to dependents:fail- Fail after max retries
Omitted-Key Contract
When a non-cascade-skipped dependency is skipped, its key is omitted from the dependent handler's input map. This allows handlers to distinguish "dependency not run" from "dependency ran but returned nil":
# If :validate is skipped (non-cascade), deps will be %{}
# If :validate ran and returned %{"valid" => false}, deps will be %{"validate" => %{"valid" => false}}
step :charge, depends_on: [:validate] do
fn deps, _ctx ->
if Map.has_key?(deps, "validate") do
# validate ran
%{charged: true}
else
# validate was skipped — this step executes but should handle the missing dep
%{charged: false, reason: :dependency_skipped}
end
end
endType Violation Exception
Retry exhaustion (:when_exhausted) does not apply to TYPE_VIOLATION errors;
such errors fail immediately regardless of retry settings.
Map Options
Map steps defined with map/2 or map/3 accept step options plus:
:array- step slug whose output array to process (for dependent maps)
Generated Functions
Using this module generates the following callback functions:
__pgflow_definition__/0- returns aPgFlow.Flow.Definitionstruct__pgflow_slug__/0- returns the flow slug atom__pgflow_steps__/0- returns the raw step definitions__pgflow_handler__/1- pattern-matched functions for each step
Runtime Flow Definition
For dynamic (runtime-defined) flows, see PgFlow.Client.upsert_flow/2, which accepts
the same conditional options as the DSL.
Summary
Functions
Defines an array processing step that executes a handler for each item.
Defines a single execution step in the workflow.
Functions
Defines an array processing step that executes a handler for each item.
Options
Map steps accept all step options (see step/3), plus:
:array- (optional) step slug whose output array to process. If omitted, the map is a root map processing the flow's input array.
Conditional options (:if, :if_not, :when_unmet, :when_exhausted) apply
to the map step itself, not to individual items.
Examples
# Root map over the flow's input array
map :process_users do
fn user, ctx ->
%{processed: process_user(user)}
end
end
# Map over output from another step
map :enrich_items, array: :fetch_items do
fn item, ctx ->
%{enriched: enrich(item)}
end
end
# Map with conditional execution (skipped if condition unmet)
map :premium_enrichment, array: :items, if: %{"plan" => "premium"} do
fn item, ctx ->
%{enriched: enrich_premium(item)}
end
end
# Map with module handler
map :validate_each, array: :items, handler: MyApp.ValidateItemHandler
# Map with custom settings
map :slow_processing, array: :items, timeout: 120 do
fn item, ctx ->
%{result: slow_process(item)}
end
endFor more on conditional execution, see the "Conditional Step Execution" section in the moduledoc.
Defines a single execution step in the workflow.
Options
See the moduledoc "Step Options" section for the complete list. Notably:
:if- Map the input/deps must match (JSON@>containment):if_not- Map the input/deps must not match:when_unmet- Action when condition fails (:fail | :skip | :skip_cascade, default:skip):when_exhausted- Action when retries exhaust (:fail | :skip | :skip_cascade, default:fail)
Examples
# Basic step with inline handler
step :fetch_data do
fn input, ctx ->
%{data: fetch_from_api(input["url"])}
end
end
# Step with dependencies
step :transform, depends_on: [:fetch_data] do
fn deps, ctx ->
%{transformed: transform(deps.fetch_data["data"])}
end
end
# Step with conditional execution (matches against input)
step :premium_step, if: %{"plan" => "premium"} do
fn input, ctx ->
%{premium_feature: true}
end
end
# Step with conditional execution (matches against dependencies)
step :charge, depends_on: [:validate], if: %{"validate" => %{"valid" => true}} do
fn deps, ctx ->
%{charged: true}
end
end
# Step with module handler
step :validate, handler: MyApp.ValidateHandler
# Step with custom retry settings
step :flaky_operation, max_attempts: 5, base_delay: 10 do
fn input, ctx ->
perform_operation()
end
endFor more on conditional execution, see the "Conditional Step Execution" section in the moduledoc.