Packages
reactor
0.15.6
1.0.2
1.0.1
1.0.0
0.17.0
0.16.0
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
An asynchronous, graph-based execution engine
Current section
Files
Jump to
Current section
Files
usage-rules.md
# Reactor Usage Rules
Reactor is a dynamic, concurrent, dependency-resolving saga orchestrator for Elixir providing saga orchestration, dependency resolution via DAG, concurrent execution, dynamic workflows, and composable DSL.
**Core concepts:** Reactors contain inputs/steps/dependencies. Steps are work units with unique names, dependencies, sync/async execution, compensation/undo capabilities. Arguments define step dependencies and data sources with optional transformations.
**Organization:** Use DSL for static workflows, `Reactor.Builder` for dynamic construction, implement `Reactor.Step` behaviour for custom steps, compose sub-reactors for complex workflows.
## DSL Syntax
```elixir
defmodule MyReactor do
use Reactor
input :data # Define inputs
step :process do # Anonymous function step
argument :data, input(:data)
run fn %{data: data}, _ -> {:ok, process(data)} end
end
step :save, MyApp.SaveStep do # Module step
argument :processed, result(:process)
end
return :save # Return result
end
```
## Step Implementation
```elixir
defmodule MyApp.Steps.CreateUser do
use Reactor.Step
def run(arguments, context, options) do
{:ok, create_user(arguments.email, arguments.password_hash)}
end
def compensate(reason, arguments, context, options) do
case reason do
%DBConnection.ConnectionError{} -> :retry # Retry on connection errors
_other -> :ok # Continue rollback
end
end
def undo(user, arguments, context, options) do
:ok = delete_user(user) # Rollback on later failure
end
end
```
## Return Values & Arguments
**Step returns:** `{:ok, value}`, `{:ok, value, [new_steps]}`, `{:error, reason}`, `:retry`, `{:halt, reason}`
**Arguments:** `argument :name, input(:input)`, `argument :name, result(:step)`, `argument :name, value(static)`
**Transforms:** `argument :id do; source result(:user); transform &(&1.id); end`
## Built-in Steps
**debug** - Log values: `debug :log do; argument :data, result(:step); end`
**map** - Process collections: `map :items do; source input(:list); step :process do; argument :item, element(:items); end; end`
**compose** - Embed reactors: `compose :sub, MyReactor do; argument :data, input(:data); end`
**switch** - Conditional: `switch :type do; on result(:data); matches? &(&1.type == :a) do; step ...; end; default do; step ...; end; end`
**group** - Shared setup: `group :ops do; before_all &setup/3; step ...; end`
**around** - Wrap execution: `around :tx, &with_transaction/4 do; step ...; end`
**collect** - Gather results: `collect :summary do; argument :a, result(:step_a); argument :b, result(:step_b); end`
## Execution Control
**Async:** Steps run async by default. Use `async? false` for sync, `async? fn opts -> condition end` for conditional.
**Guards:** `guard &(&1.user.active?)` or `where fn %{user: user} -> user.plan == :premium end`
**Dependencies:** `wait_for :other_step` for ordering without data flow
**Halting:** Step returns `{:halt, reason}` → `{:halted, state} = Reactor.run(...)` → `Reactor.run(state, %{}, %{})`
**Running:** `Reactor.run(MyReactor, inputs, context, opts)`
## Middleware
```elixir
defmodule MyApp.LoggingMiddleware do
use Reactor.Middleware
def init(context), do: {:ok, context}
def complete(result, context), do: {:ok, result}
def error(errors, context), do: :ok
def event({:run_start, args}, step, context), do: log_step_start(step, args)
end
# Usage in reactor
middlewares do
middleware MyApp.LoggingMiddleware
middleware Reactor.Middleware.Telemetry
end
```
## Advanced Features
**Context:** Pass data through context: `def run(arguments, context, options)` where `context[:current_user]`
**Transforms:** Apply to inputs/arguments: `input :date do; transform &Date.from_iso8601!/1; end`
**Retries:** `max_retries 3` with `compensate` returning `:retry`
**Composition:** `compose :sub, SubReactor do; argument :data, result(:prep); end`
## Best Practices
**Error Handling:** Implement `compensate/4` for retryable errors, `undo/4` for rollback. Use specific error types. Work transactionally when possible. Use `Ash.Reactor` steps for Ash apps.
**Performance:** Use `async? false` sparingly, set `batch_size` for maps, use `strict_ordering? false` when order doesn't matter, limit `max_concurrency` for constrained resources.
**Organization:** Create reusable step modules, compose sub-reactors for complex workflows, group related steps, use meaningful names.
**Testing:** Test steps in isolation, test various input combinations and error scenarios, use `async? false` for deterministic tests.
**Debugging:** Use debug steps, add telemetry middleware, use descriptive names, test with `async? false` to simplify.