Building Iterative Workflows with Recursive Execution
View SourceIn this tutorial, you'll learn how to build iterative workflows that process data repeatedly until a condition is met. This is perfect for mathematical algorithms, data processing pipelines, and convergence calculations.
What you'll build
Simple iterative algorithms demonstrating recursive patterns:
- Countdown Calculator - Count down from a number to zero
- List Processor - Process items one at a time until done
You'll learn
- How Reactor's recursive execution works
- Setting up proper exit conditions and iteration limits
- Managing state between iterations
- When to use recursion vs other Reactor patterns
- Building convergent algorithms and iterative processors
Prerequisites
- Complete the Getting Started tutorial
- Complete the Error Handling tutorial
- Complete the Async Workflows tutorial
- Complete the Composition tutorial
Step 1: Set up the project
If you don't have a project from the previous tutorials:
mix igniter.new reactor_tutorial --install reactor
cd reactor_tutorial
Step 2: Understanding recursive execution
Reactor's recursive execution allows you to:
Repeat until a condition is met:
recurse :calculate, CalculatorReactor do
argument :value, input(:start_value)
exit_condition fn %{done: done} -> done == true end
max_iterations 100
end
Process data iteratively:
recurse :process, ProcessorReactor do
argument :remaining, input(:data_list)
exit_condition fn %{remaining: list} -> Enum.empty?(list) end
max_iterations 1000
end
Step 3: Build a simple countdown reactor
Let's start with a simple recursive algorithm. Create lib/countdown_reactor.ex
:
defmodule CountdownReactor do
use Reactor
input :current_number
step :countdown_step do
argument :current_number, input(:current_number)
run fn %{current_number: num}, _context ->
new_number = num - 1
{:ok, %{current_number: new_number}}
end
end
return :countdown_step
end
defmodule CountdownExample do
use Reactor
input :start_number
recurse :countdown, CountdownReactor do
argument :current_number, input(:start_number)
max_iterations 100
exit_condition fn %{current_number: num} -> num <= 0 end
end
step :show_result do
argument :final_state, result(:countdown)
argument :start_number, input(:start_number)
run fn %{final_state: state, start_number: start}, _context ->
result = %{
started_at: start,
finished_at: state.current_number,
message: "Counted down from #{start} to #{state.current_number}"
}
{:ok, result}
end
end
return :show_result
end
Step 4: Build an accumulator
Create lib/accumulator.ex
:
defmodule AccumulatorReactor do
use Reactor
input :current_total
input :target_total
step :add_random_amount do
argument :current_total, input(:current_total)
argument :target_total, input(:target_total)
run fn %{current_total: current, target_total: target}, _context ->
# Add a random amount between 1 and 10
addition = :rand.uniform(10)
new_total = current + addition
reached_target = new_total >= target
{:ok, %{
current_total: new_total,
target_total: target,
last_addition: addition,
reached_target: reached_target
}}
end
end
return :add_random_amount
end
defmodule AccumulatorExample do
use Reactor
input :target_score
recurse :accumulate, AccumulatorReactor do
argument :current_total, value(0)
argument :target_total, input(:target_score)
max_iterations 50
exit_condition fn %{reached_target: reached} -> reached == true end
end
step :show_results do
argument :final_state, result(:accumulate)
argument :target, input(:target_score)
run fn %{final_state: state, target: target}, _context ->
result = %{
target_score: target,
final_total: state.current_total,
last_addition: state.last_addition,
message: "Reached #{state.current_total} (target was #{target})"
}
{:ok, result}
end
end
return :show_results
end
Step 5: Advanced example - Convergence algorithm
Now let's look at a more sophisticated pattern - iterative convergence. Create lib/square_root_calculator.ex
:
defmodule NewtonMethodReactor do
use Reactor
input :current_guess
input :target_number
step :newton_iteration do
argument :current_guess, input(:current_guess)
argument :target_number, input(:target_number)
run fn %{current_guess: guess, target_number: target}, _context ->
# Newton's method: x_new = (x + target/x) / 2
new_guess = (guess + target / guess) / 2
difference = abs(new_guess - guess)
# Consider converged when difference is very small
converged = difference < 0.0001
{:ok, %{
current_guess: new_guess,
target_number: target,
converged: converged
}}
end
end
return :newton_iteration
end
defmodule SquareRootCalculator do
use Reactor
input :number
recurse :converge, NewtonMethodReactor do
argument :current_guess, input(:number) # Start with the number itself
argument :target_number, input(:number)
max_iterations 20
exit_condition fn %{converged: converged} -> converged == true end
end
step :show_result do
argument :final_state, result(:converge)
argument :original_number, input(:number)
run fn %{final_state: state, original_number: num}, _context ->
result = %{
number: num,
square_root: state.current_guess,
message: "√#{num} ≈ #{Float.round(state.current_guess, 6)}"
}
{:ok, result}
end
end
return :show_result
end
Step 6: Test the examples
Let's test our recursive reactors:
iex -S mix
# Test the countdown example
{:ok, result} = Reactor.run(CountdownExample, %{start_number: 5})
IO.inspect(result.message)
# Should output: "Counted down from 5 to 0"
# Test the accumulator
{:ok, result} = Reactor.run(AccumulatorExample, %{target_score: 50})
IO.inspect(result.message)
# Should output something like: "Reached 53 (target was 50)"
# Test the square root calculator
{:ok, result} = Reactor.run(SquareRootCalculator, %{number: 25})
IO.inspect(result.message)
# Should output: "√25 ≈ 5.0"
What you learned
You now understand Reactor's recursive execution:
- Always provide termination conditions - Use both
exit_condition
andmax_iterations
- Keep state simple - Use flat maps with simple values
- Test with small examples - Start simple before building complexity
- Convergence detection - Check when iterative improvements become negligible
When to use recursion:
Use recursion for:
- Counting and decrementing patterns
- Accumulation until a threshold is reached
- Iterative calculations with clear stopping conditions
- Mathematical convergence algorithms
Avoid recursion for:
- Simple sequential processing (use regular steps)
- One-time transformations (use map steps)
- Parallel processing of collections (use map steps with batching)
What's next
You've mastered all core Reactor patterns! Ready for specialized guides:
- Testing Strategies - Comprehensive testing approaches
- Performance Optimization - Advanced performance techniques
- Debugging Workflows - Troubleshooting complex reactors
Common issues
Recursion never terminates: Ensure exit conditions can actually be met; always include max_iterations
as backup
"Maximum iterations exceeded" error: Increase max_iterations
or fix logic preventing proper convergence
State not passing correctly: Ensure output structure exactly matches input requirements; keep state flat and simple
Exit condition never triggers: Add debug logging to verify exit condition logic; check that condition field exists in state
Happy building iterative workflows! 🔄