### Testing Reactor Examples Source: https://github.com/ash-project/reactor/blob/main/documentation/tutorials/05-recursive-execution.md Provides example commands and Elixir code to test the implemented Reactor examples, including the square root calculator. ```bash iex -S mix ``` ```elixir # 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" ``` -------------------------------- ### Start IEx Session Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/api-orchestration.md Starts an IEx (Interactive Elixir) session within a Mix project. This is the entry point for running Elixir code and testing your reactors. ```bash iex -S mix ``` -------------------------------- ### Avoid Excessive Dependencies in Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/performance-optimization.md This example illustrates how to redesign Reactor steps to maximize parallelization. The 'GOOD' example shows independent steps running in parallel, with results combined only when necessary, avoiding artificial sequential dependencies. ```elixir # ❌ BAD: Artificial sequential dependencies step :step1, do: run(fn -> {:ok, data1} end) step :step2 do argument :data1, result(:step1) # Unnecessary dependency run fn %{data1: _} -> {:ok, data2} end # Doesn't actually use data1 end step :step3 do argument :data2, result(:step2) # Creates sequential chain run fn %{data2: _} -> {:ok, data3} end end # ✅ GOOD: Independent steps run in parallel step :step1, do: run(fn -> {:ok, data1} end) step :step2, do: run(fn -> {:ok, data2} end) # No dependency on step1 step :step3, do: run(fn -> {:ok, data3} end) # No dependency on step2 # Combine results only when needed step :combine_results do argument :data1, result(:step1) argument :data2, result(:step2) argument :data3, result(:step3) run fn args -> {:ok, combine(args)} end end ``` -------------------------------- ### Define a step to create a user Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example shows how to define a Reactor step for creating a user, specifying arguments like username and a pre-hashed password. ```elixir step :create_user, MyApp.Steps.CreateUser do argument :username, input(:username) argument :password_hash, result(:hash_password) end ``` -------------------------------- ### Define Reactor Programmatically Source: https://github.com/ash-project/reactor/blob/main/CLAUDE.md Example of defining a Reactor workflow programmatically using `Reactor.Builder`. This offers an alternative to the DSL. ```elixir Reactor.Builder ``` -------------------------------- ### Define a map argument from input Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example shows how to define an argument for a `map` step, sourcing its value directly from a named input. ```elixir argument :name, input(:name) ``` -------------------------------- ### Define a map argument from a previous result Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example shows how to define a map argument using the output of a previous step named 'create_user'. ```elixir argument :user, result(:create_user) ``` -------------------------------- ### Define HTTP Client with Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/api-orchestration.md Set up a basic HTTP client using reactor_req, defining inputs, request setup, and data fetching steps. ```elixir defmodule ApiClient do use Reactor input :base_url input :user_id req_new :setup_client do base_url input(:base_url) headers value([{"user-agent", "MyApp/1.0"}]) retry value(:transient) retry_delay value(fn attempt -> 200 * attempt end) end template :build_profile_url do argument :user_id, input(:user_id) template "/users/<%= @user_id %>" end template :build_preferences_url do argument :user_id, input(:user_id) template "/users/<%= @user_id %>/preferences" end req_get :fetch_profile do request result(:setup_client) url result(:build_profile_url) headers value(%{"accept" => "application/json"}) end req_get :fetch_preferences do request result(:setup_client) url result(:build_preferences_url) end step :combine_data do argument :profile, result(:fetch_profile, [:body]) argument :preferences, result(:fetch_preferences, [:body]) run fn %{profile: profile, preferences: prefs}, _context -> {:ok, %{profile: profile, preferences: prefs}} end end return :combine_data end ``` -------------------------------- ### Integrate Reactor Ecosystem Packages Source: https://github.com/ash-project/reactor/blob/main/documentation/explanation/ecosystem.md This example shows how to use Reactor with extensions from its ecosystem packages (Req, File, Process) to perform file operations, HTTP requests, and process management. ```elixir defmodule MyWorkflowReactor do use Reactor, extensions: [Reactor.Req, Reactor.File, Reactor.Process] input :file_path input :upload_url # File operations from reactor_file (DSL entities) file_read :process_file do path input(:file_path) end # HTTP requests from reactor_req (DSL entities) req_post :upload_data do url input(:upload_url) body result(:process_file) end # Process management from reactor_process (DSL entities) start_child :start_worker do supervisor value(MyApp.WorkerSupervisor) child_spec template({MyApp.Worker, result(:process_file)}) end return :upload_data end ``` -------------------------------- ### Avoid Blocking Async Steps in Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/performance-optimization.md This example demonstrates how to refactor a Reactor step that involves blocking operations. The 'GOOD' and 'BETTER' examples show how to model dependencies explicitly or use recursive steps to avoid wasting concurrency slots. ```elixir # ❌ BAD: Blocking because of missing dependency step :process_data do run fn args, _context -> # Blocking while waiting for external resource wait_for_resource_to_be_ready() # This wastes concurrency slots process_with_resource(args) end end # ✅ GOOD: Model the dependency explicitly with separate steps step :prepare_resource do run fn _args, _context -> # This step ensures the resource is ready setup_resource() {:ok, :resource_ready} end end step :process_data do argument :resource_ready, result(:prepare_resource) argument :data, input(:data) run fn %{data: data}, _context -> # No blocking - resource dependency ensures it's ready process_with_resource(data) end end # ✅ BETTER: Use recursive step for external resources step :wait_for_external_service do run fn args, context -> case check_external_service() do {:ok, result} -> {:ok, result} {:error, :not_ready} -> # Emit the current step again - Reactor will retry when it can current_step = context.current_step {:ok, nil, [current_step]} end end end ``` -------------------------------- ### Use Built-in Group Step Source: https://github.com/ash-project/reactor/blob/main/usage-rules.md The 'group' step allows defining setup logic ('before_all') that runs once before a set of steps within the group. ```elixir group :ops do; before_all &setup/3; step ...; end ``` -------------------------------- ### Ash Framework Integration with Ash.Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/explanation/ecosystem.md This example demonstrates using the Ash.Reactor extension to define a user onboarding workflow involving Ash resource actions like create, update, and sending notifications. ```elixir defmodule UserOnboardingReactor do # Using the Ash.Reactor extension use Ash.Reactor input :user_params # Ash-specific create action step create :user, MyApp.User, :create do inputs %{ email: input(:user_params, [:email]), name: input(:user_params, [:name]) } end # Ash-specific update action step update :activate_user, MyApp.User, :activate do record result(:user) inputs %{activated_at: value(DateTime.utc_now())} end # Ash action with automatic undo support action :send_welcome_email, MyApp.Notifications, :send_welcome do inputs %{user_id: result(:user, :id)} undo :outside_transaction end return :activate_user end ``` -------------------------------- ### Accumulator Example Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/tutorials/05-recursive-execution.md An example Reactor that uses the AccumulatorReactor to reach a target score by repeatedly adding random amounts. It configures the recursion and returns the final accumulated state. ```elixir 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 ``` -------------------------------- ### Countdown Example Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/tutorials/05-recursive-execution.md An example Reactor that uses the CountdownReactor to count down from a start number to zero. It defines the recursion parameters and returns the final state. ```elixir 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 ``` -------------------------------- ### Compare Backoff Strategies with Benchee Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/performance-optimization.md Compares the performance of no backoff versus a fixed 100ms backoff strategy using Benchee. Requires a simulated FlakeyAPI and Reactor setup. ```elixir defmodule BackoffBenchmark do def compare_backoff_strategies do # Simulate API that fails 30% of the time defmodule FlakeyAPI do use Reactor.Step @impl true def run(_args, _context, _options) do if :rand.uniform() < 0.3 do {:error, %{type: :transient_failure}} else {:ok, "success"} end end @impl true def compensate(_error, _args, _context, _options), do: :retry end # No backoff strategy defmodule NoBackoffAPI do use Reactor.Step defdelegate run(args, context, options), to: FlakeyAPI defdelegate compensate(error, args, context, options), to: FlakeyAPI @impl true def backoff(_error, _args, _context, _step), do: :now end # Fixed backoff strategy defmodule FixedBackoffAPI do use Reactor.Step defdelegate run(args, context, options), to: FlakeyAPI defdelegate compensate(error, args, context, options), to: FlakeyAPI @impl true def backoff(_error, _args, _context, _step), do: 100 # 100ms fixed delay end data = Enum.to_list(1..100) Benchee.run( %{ "no_backoff" => fn -> reactor = build_reactor(NoBackoffAPI, data) Reactor.run(reactor, %{items: data}, %{}, max_concurrency: 10) end, "fixed_backoff" => fn -> reactor = build_reactor(FixedBackoffAPI, data) Reactor.run(reactor, %{items: data}, %{}, max_concurrency: 10) end }, warmup: 1, time: 3 ) end end ``` -------------------------------- ### Create New Project with Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/tutorials/01-getting-started.md Use this command to create a new Elixir project with Reactor installed and configured. It adds Reactor to dependencies and sets up necessary configurations. ```bash mix igniter.new reactor_tutorial --install reactor cd reactor_tutorial ``` -------------------------------- ### Configure Telemetry Middleware Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/payment-processing.md Example of how to include the Reactor Telemetry middleware in a workflow definition. This is useful for observability. ```elixir defmodule ECommerce.PaymentWorkflow do use Reactor middlewares do middleware Reactor.Middleware.Telemetry end # ... steps end ``` -------------------------------- ### Map and cancel subscriptions Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example demonstrates mapping over a list of subscriptions obtained from `Stripe.Subscription.list()`. For each subscription, it cancels it using `Stripe.Subscription.cancel` with specific options. ```elixir step :get_subscriptions do run fn _, _ -> Stripe.Subscription.list() end end map :cancel_subscriptions do source result(:get_subscriptions) step :cancel do argument :sub_id, element(:cancel_subscriptions, [:id]) run fn args, _ -> Stripe.Subscription.cancel(arg.sub_id, %{prorate: true, invoice_now: true}) end end return :cancel end ``` -------------------------------- ### Setup Mimic for Step Mocking Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/testing-strategies.md Configures Mimic in test_helper.exs to copy step modules for mocking. This allows stubbing function calls without altering production code. ```elixir # test/test_helper.exs Mimic.copy(MyApp.Steps.ProcessPayment) Mimic.copy(MyApp.Steps.ReserveInventory) Mimic.copy(MyApp.Steps.SendConfirmation) ExUnit.start() ``` -------------------------------- ### reactor.compose.wait_for Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Specifies that a step should wait for one or more named steps to complete before starting. ```APIDOC ## reactor.compose.wait_for ### Description Wait for the named step to complete before allowing this one to start. Desugars to `argument :_, result(step_to_wait_for)`. ### Arguments #### Path Parameters - **names** (atom | list(atom)) - Required - The name of the step to wait for. #### Query Parameters - **description** (String.t) - Optional - An optional description. ### Request Example ```elixir wait_for :create_user ``` ``` -------------------------------- ### Run Successful Workflow Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/payment-processing.md Example of successfully running a Reactor workflow with valid inputs. The result is pattern matched for success. ```elixir # Successful execution {:ok, confirmation} = Reactor.run(ECommerce.PaymentWorkflow, order_id: "order_123", payment_details: %{ card_number: "4111111111111111", exp_month: 12, exp_year: 2025, cvv: "123" }, customer_id: "customer_456" ) ``` -------------------------------- ### Define a map argument from a static value Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example demonstrates defining a map argument using a static, hardcoded value. ```elixir argument :three, value(3) ``` -------------------------------- ### Batch Size Guidelines by Data Characteristics Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/performance-optimization.md Provides example batch sizes based on input record and output result sizes. Use larger batch sizes for small data and smaller sizes for large data to manage memory. ```elixir # For small input records, small output results batch_size 1000 # Safe - total memory stays manageable # For large input records OR large output results batch_size 10 # Conservative - limits memory multiplication # For very large transformations (e.g., image processing) batch_size 1 # Process one at a time to avoid memory spikes # Consider your total dataset size: # 1M records × 100 batch size = 100K steps in memory at once # 1M records × 10 batch size = 100K steps total, 10K at once ``` -------------------------------- ### Define a Generic Workflow Reactor Source: https://github.com/ash-project/reactor/blob/main/documentation/explanation/ecosystem.md This example demonstrates how to define a Reactor workflow using generic Elixir code, integrating with GenServer, HTTPoison, and ETS. ```elixir defmodule GenericWorkflowReactor do use Reactor input :data step :process_with_genserver do argument :data, input(:data) run fn args, _context -> GenServer.call(MyService, {:process, args.data}) end end step :call_external_api do argument :processed_data, result(:process_with_genserver) run fn args, _context -> HTTPoison.post("https://api.example.com", args.processed_data) end end step :store_in_ets do argument :api_response, result(:call_external_api) run fn args, _context -> :ets.insert(:my_table, {:result, args.api_response}) {:ok, :stored} end end return :store_in_ets end ``` -------------------------------- ### Reactor as Ash Action Implementation Source: https://github.com/ash-project/reactor/blob/main/documentation/explanation/ecosystem.md Implement Ash actions using a Reactor module, defining inputs and orchestrating resource creation and notification sending. This example assumes MyApp.User and MyApp.Notifications modules exist. ```elixir # Reactor as Ash action implementation defmodule MyApp.UserRegistrationReactor do use Ash.Reactor input :email input :password input :name create :user, MyApp.User, :create do inputs %{ email: input(:email), name: input(:name) } end action :send_welcome_email, MyApp.Notifications, :send_welcome do inputs %{user_id: result(:user, :id)} end return :user end ``` -------------------------------- ### Define a map argument from input with selection Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example demonstrates defining a map argument by selecting a specific field from a nested input structure. ```elixir argument :year, input(:date, [:year]) ``` -------------------------------- ### Define a map argument from a result with selection Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example shows how to define a map argument by selecting a specific field ('id') from the result of a previous step. ```elixir argument :user_id, result(:create_user, [:id]) ``` -------------------------------- ### Define Reactor via DSL Source: https://github.com/ash-project/reactor/blob/main/CLAUDE.md Example of defining a Reactor workflow using the provided DSL. This is one of the primary ways to define a Reactor. ```elixir use Reactor ``` -------------------------------- ### Debug Step Not Executing Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/debugging-workflows.md Example of using a `debug` block before a problematic step to check dependencies and data flow. Ensure that the `data` argument is correctly passed from the `previous_step`. ```elixir # Check if dependencies are met debug :before_problem_step do argument :data, result(:previous_step) end step :problem_step do argument :data, result(:previous_step) # ... end ``` -------------------------------- ### Error Recovery Strategy Example Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/payment-processing.md Illustrates a comprehensive error recovery strategy within a `compensate` function. It differentiates between transient errors that should be retried and business logic errors that should not. ```elixir def compensate(reason, arguments, context, options) do case reason do # Retry transient failures %HTTPoison.Error{reason: :timeout} -> :retry %HTTPoison.Error{reason: :econnrefused} -> :retry %PaymentGateway.NetworkError{} -> :retry # Don't retry business logic failures %PaymentGateway.CardDeclinedError{} -> :ok %InventoryService.OutOfStockError{} -> :ok # Default: don't retry unknown errors _other -> :ok end end ``` -------------------------------- ### reactor.step.wait_for Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Waits for a named step to complete before allowing the current one to start. ```APIDOC ## POST /reactor/step/wait_for ### Description Wait for the named step to complete before allowing this one to start. Desugars to `argument :_, result(step_to_wait_for)`. ### Method POST ### Endpoint `/reactor/step/wait_for` ### Parameters #### Query Parameters - **names** (atom | list(atom)) - Required - The name of the step to wait for. #### Options - **description** (String.t) - Optional - An optional description. ``` -------------------------------- ### reactor.map.wait_for Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Waits for a named step to complete before allowing the current step to start. ```APIDOC ## POST /reactor/map/wait_for ### Description Wait for the named step to complete before allowing this one to start. Desugars to `argument :_, result(step_to_wait_for)`. ### Method POST ### Endpoint `/reactor/map/wait_for` ### Arguments #### names - **names** (atom | list(atom)) - Required - The name of the step to wait for. ### Options #### description - **description** (String.t) - Optional - An optional description. ``` -------------------------------- ### Set Up Reactor Telemetry Debugging Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/debugging-workflows.md Attaches telemetry event handlers to monitor reactor start/stop and step execution. Requires the :telemetry library. ```elixir defmodule MyApp.ReactorTelemetry do def setup_debugging() do events = [ [:reactor, :run, :start], [:reactor, :run, :stop], [:reactor, :step, :run, :start], [:reactor, :step, :run, :stop] ] :telemetry.attach_many( "reactor-debug", events, &handle_event/4, nil ) end def handle_event([:reactor, :run, :start], _measurements, metadata, _config) do IO.puts("🚀 Starting reactor: #{metadata.reactor.id}") end def handle_event([:reactor, :run, :stop], measurements, metadata, _config) do duration_ms = measurements.duration / 1_000_000 case metadata.outcome do :ok -> IO.puts("✅ Reactor completed in #{duration_ms}ms") :error -> IO.puts("❌ Reactor failed in #{duration_ms}ms") :halt -> IO.puts("⏸️ Reactor halted in #{duration_ms}ms") end end def handle_event([:reactor, :step, :run, :start], _measurements, metadata, _config) do IO.puts(" 🔄 Starting step: #{metadata.step.name}") end def handle_event([:reactor, :step, :run, :stop], measurements, metadata, _config) do duration_ms = measurements.duration / 1_000_000 case metadata.outcome do :ok -> IO.puts(" ✅ Step #{metadata.step.name} completed in #{duration_ms}ms") :error -> IO.puts(" ❌ Step #{metadata.step.name} failed in #{duration_ms}ms") :retry -> IO.puts(" 🔄 Step #{metadata.step.name} retrying after #{duration_ms}ms") end end end ``` -------------------------------- ### Reactor Wait For Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Specifies that a step should wait for one or more other steps to complete before starting. ```APIDOC ## POST /reactor/collect/wait_for ### Description Waits for the named step(s) to complete before allowing the current step to start. ### Method POST ### Endpoint /reactor/collect/wait_for ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **names** (atom | list(atom)) - Required - The name(s) of the step(s) to wait for. - **description** (String.t) - Optional - An optional description of the wait condition. ### Request Example ```json { "names": ["step1", "step2"], "description": "Wait for initial data processing" } ``` ### Response #### Success Response (200) - **status** (string) - Indicates success. #### Response Example ```json { "status": "success" } ``` ``` -------------------------------- ### Run User Registration Reactor (Valid Data) Source: https://github.com/ash-project/reactor/blob/main/documentation/tutorials/01-getting-started.md Execute the UserRegistration reactor with valid email and password inputs in an IEx session. Inspect the resulting user map, which includes a generated ID, email, hashed password, and creation timestamp. ```elixir iex -S mix ``` ```elixir # Test with valid data {:ok, user} = Reactor.run(UserRegistration, %{ email: "alice@example.com", password: "secret123" }) IO.inspect(user) ``` -------------------------------- ### Reactor Wait For Step Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Specifies that a step should wait for another named step to complete before starting. ```APIDOC ## POST /reactor/wait_for ### Description Waits for the named step to complete before allowing the current step to start. This desugars to `argument :_, result(step_to_wait_for)`. ### Method POST ### Endpoint /reactor/wait_for ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **names** (atom | list(atom)) - Required - The name of the step(s) to wait for. - **description** (String.t) - Optional - An optional description. ``` -------------------------------- ### reactor.group.wait_for Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Wait for the named step to complete before allowing this one to start. Desugars to `argument :_, result(step_to_wait_for)`. ```APIDOC ## POST /reactor/group/wait_for ### Description Wait for the named step to complete before allowing this one to start. This is achieved by creating an argument that depends on the result of the specified step. ### Method POST ### Endpoint /reactor/group/wait_for ### Parameters #### Request Body - **names** (atom | list(atom)) - Required - The name of the step(s) to wait for. - **description** (String.t) - Optional - An optional description of the wait condition. ### Request Example ```json { "names": "create_user", "description": "Waiting for user creation to complete" } ``` ### Response #### Success Response (200) - **status** (string) - Indicates the success of the operation. #### Response Example ```json { "status": "Wait for condition set successfully" } ``` ``` -------------------------------- ### reactor.debug.wait_for Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Wait for the named step to complete before allowing this one to start. Desugars to `argument :_, result(step_to_wait_for)`. ```APIDOC ## reactor.debug.wait_for ### Description Wait for the named step to complete before allowing this one to start. Desugars to `argument :_, result(step_to_wait_for)` ### Arguments #### Path Parameters - **names** (atom | list(atom)) - Required - The name of the step to wait for. ### Options #### Query Parameters - **description** (String.t) - Optional - An optional description. ### Examples ```elixir wait_for :create_user ``` ``` -------------------------------- ### reactor.around.wait_for Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Waits for a named step to complete before allowing the current step to start. This desugars to `argument :_, result(step_to_wait_for)`. ```APIDOC ## POST reactor.around.wait_for ### Description Wait for the named step to complete before allowing this one to start. ### Method POST ### Endpoint /reactor/around/wait_for ### Parameters #### Query Parameters - **names** (atom | list(atom)) - Required - The name of the step to wait for. #### Request Body - **description** (String.t) - Optional - An optional description. ### Response #### Success Response (200) - **status** (string) - Indicates success. #### Response Example ```json { "status": "success" } ``` ``` -------------------------------- ### Debug Unexpected Results by Comparing Expected vs Actual Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/debugging-workflows.md Illustrates how to use `debug` and `step` blocks to compare the actual result of a calculation step against an expected value. This helps in identifying discrepancies in logic. ```elixir # Compare expected vs actual debug :validate_result do argument :result, result(:calculation_step) end step :verify_result do argument :result, result(:calculation_step) run fn %{result: result}, _context -> expected = 42 if result == expected do {:ok, result} else {:error, "Expected #{expected}, got #{result}"} end end end ``` -------------------------------- ### reactor.template.wait_for Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Waits for a named step to complete before allowing the current step to start. This is useful for managing dependencies between steps in a process. ```APIDOC ## POST /reactor/template/wait_for ### Description Wait for the named step to complete before allowing this one to start. ### Method POST ### Endpoint /reactor/template/wait_for ### Parameters #### Request Body - **names** (atom | list(atom)) - Required - The name of the step to wait for. - **description** (String.t) - Optional - An optional description. ``` -------------------------------- ### Sequential vs. Dependency-Driven Workflow in Elixir Source: https://github.com/ash-project/reactor/blob/main/documentation/explanation/design-decisions.md Illustrates the difference between traditional explicit step sequencing and Reactor's dependency-driven approach for workflow execution. ```elixir # Sequential workflow - explicit ordering step_1 → step_2 → step_3 → step_4 ``` ```elixir # Dependency-driven workflow - automatic ordering defmodule UserWorkflow do use Reactor step :fetch_user do argument :user_id, input(:user_id) end step :fetch_profile do argument :user_id, input(:user_id) # Can run concurrently with fetch_user end step :merge_data do argument :user, result(:fetch_user) # Waits for fetch_user argument :profile, result(:fetch_profile) # Waits for fetch_profile end end ``` -------------------------------- ### Control Step Execution Order Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/payment-processing.md Shows how to use `wait_for` to ensure one step completes before another begins, without necessarily passing data between them. This is useful for managing dependencies. ```elixir step :authorize_payment do argument :payment_details, input(:payment_details) wait_for :reserve_inventory # Ensure inventory reserved first run &authorize_payment/1 end ``` -------------------------------- ### Define an input with transformation Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Declare an input and apply a transformation function to its value before it's used. The example converts the input to an integer. ```elixir input :age do transform &String.to_integer/1 end ``` -------------------------------- ### Generate Documentation Source: https://github.com/ash-project/reactor/blob/main/CLAUDE.md Command to generate project documentation. This command also runs `spark.cheat_sheets` and `spark.replace_doc_links`. ```bash mix docs ``` -------------------------------- ### Create an EEx template step Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Use `reactor.template` to pass arguments into an EEx template and return the rendered result. This step is useful for generating dynamic content. ```elixir template :welcome_message do arguments :user template """ Welcome <%= @user.name %>! 🎉 """ end ``` -------------------------------- ### Run All Quality Checks Source: https://github.com/ash-project/reactor/blob/main/CLAUDE.md Preferred command to run all quality checks including formatter, credo, dialyzer, and tests. Use this for general development. ```bash mix check --no-retry ``` -------------------------------- ### Set Up Performance Monitoring for Reactor Steps Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/debugging-workflows.md Attaches a telemetry handler to monitor the duration of reactor steps. Configure the slow threshold in milliseconds to detect performance bottlenecks. ```elixir defmodule PerformanceMonitor do def setup_performance_monitoring(slow_threshold_ms \ 100) do :telemetry.attach( "reactor-performance", [:reactor, :step, :run, :stop], &monitor_step_performance/4, %{threshold: slow_threshold_ms * 1_000_000} # Convert to nanoseconds ) end def monitor_step_performance(_event, measurements, metadata, config) do if measurements.duration > config.threshold do duration_ms = measurements.duration / 1_000_000 IO.puts("🐌 Slow step detected:") IO.puts(" Step: #{metadata.step.name}") IO.puts(" Duration: #{duration_ms}ms") IO.puts(" Module: #{metadata.step.impl}") end end end PerformanceMonitor.setup_performance_monitoring(50) # 50ms threshold ``` -------------------------------- ### Create InventoryReactor Source: https://github.com/ash-project/reactor/blob/main/documentation/tutorials/04-composition.md This reactor checks product availability and reserves items based on product ID and quantity. It requires product_id and quantity as inputs. ```elixir defmodule InventoryReactor do use Reactor input :product_id input :quantity step :check_availability do argument :product_id, input(:product_id) argument :quantity, input(:quantity) run fn %{product_id: product_id, quantity: quantity}, _context -> available = 50 if quantity <= available do {:ok, %{product_id: product_id, available: available}} else {:error, "Not enough inventory"} end end end step :reserve_items do argument :availability, result(:check_availability) argument :quantity, input(:quantity) run fn %{availability: avail, quantity: qty}, _context -> reservation = %{ product_id: avail.product_id, quantity: qty, reserved_at: DateTime.utc_now() } {:ok, reservation} end end return :reserve_items end ``` -------------------------------- ### Elixir Load Data to Database Step Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/data-pipelines.md Loads cleaned user data into a database in batches of 1000. It uses `insert_all` with conflict resolution based on email. Includes a `compensate` function for cleanup on failure. ```elixir defmodule DataPipeline.Steps.LoadToDatabase do use Reactor.Step @impl true def run(%{users: users}, _context, _options) do batches = Enum.chunk_every(users, 1000) results = Enum.map(batches, fn batch -> case MyApp.Repo.insert_all("users", batch, on_conflict: :replace_all, conflict_target: [:email]) do {count, _} -> {:ok, count} error -> {:error, error} end end) total_inserted = results |> Enum.filter(&match?({:ok, _}, &1)) |> Enum.map(&elem(&1, 1)) |> Enum.sum() {:ok, %{inserted: total_inserted, total_batches: length(batches)}} end @impl true def compensate(_reason, %{users: users}, _context, _options) do # Cleanup on failure - remove any partially inserted data user_emails = Enum.map(users, & &1.email) MyApp.Repo.delete_all(from u in "users", where: u.email in ^user_emails) :ok end end ``` -------------------------------- ### Define Backoff in DSL Steps with Anonymous Functions Source: https://github.com/ash-project/reactor/blob/main/documentation/tutorials/02-error-handling.md Demonstrates defining backoff logic directly within DSL steps using anonymous functions for `run`, `compensate`, and `backoff`. This approach is used when implementing steps with anonymous functions, not with implementation modules. ```elixir defmodule BackoffUserRegistration do use Reactor input :email input :password step :validate_email do argument :email, input(:email) run fn %{email: email}, _context -> if String.contains?(email, "@") and String.length(email) > 5 do {:ok, email} else {:error, "Email must contain @ and be longer than 5 characters"} end end end step :hash_password do argument :password, input(:password) run fn %{password: password}, _context -> if String.length(password) >= 8 do hashed = :crypto.hash(:sha256, password) |> Base.encode16() {:ok, hashed} else {:error, "Password must be at least 8 characters"} end end end step :create_user do argument :email, result(:validate_email) argument :password_hash, result(:hash_password) max_retries 3 run fn %{email: email, password_hash: hash}, _context -> user = %{ id: :rand.uniform(10000), email: email, password_hash: hash, created_at: DateTime.utc_now() } {:ok, user} end compensate fn _error, _args, _context -> :retry # Database errors are usually retryable end # DSL backoff function (only available with anonymous run functions) backoff fn _error, _args, context -> retry_count = Map.get(context, :current_try, 0) # Exponential backoff: 1s, 2s, 4s, 8s... delay = :math.pow(2, retry_count) * 1000 |> round() IO.puts("🔄 Database retry #{retry_count + 1} - waiting #{delay}ms") delay end end step :send_welcome_email, EmailService do argument :email, result(:validate_email) argument :user, result(:create_user) max_retries 3 # EmailService module has its own backoff/4 callback end step :send_admin_notification, NotificationService do argument :user, result(:create_user) max_retries 1 end return :create_user end ``` -------------------------------- ### Transform Arguments Before Passing to Steps Source: https://github.com/ash-project/reactor/blob/main/documentation/explanation/concepts.md Arguments can be transformed using a provided function before being passed to a step. This example shows converting an email argument to lowercase. ```elixir step :send_email do argument :email, result(:validate_email) do transform &String.downcase/1 end end ``` -------------------------------- ### Run Tests Source: https://github.com/ash-project/reactor/blob/main/CLAUDE.md Commands to run tests. Can run all tests, a single test file, or a specific test at a given line number. ```bash mix test ``` ```bash mix test test/reactor/step_test.exs ``` ```bash mix test test/reactor/step_test.exs:14 ``` -------------------------------- ### Spark DSL Formatting Source: https://github.com/ash-project/reactor/blob/main/CLAUDE.md Command to format the Spark DSL with specified extensions. Ensure the `.formatter.exs` file is configured for Spark DSL formatting. ```bash mix spark.formatter --extensions Reactor.Dsl ``` -------------------------------- ### Use Built-in Around Step Source: https://github.com/ash-project/reactor/blob/main/usage-rules.md The 'around' step wraps a set of steps with provided setup and teardown logic, similar to a transaction or context manager. ```elixir around :tx, &with_transaction/4 do; step ...; end ``` -------------------------------- ### Reactor 'argument' definition Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Shows how to define arguments for Reactor steps, sourcing them from inputs, results, or literal values. ```APIDOC ## POST /reactor/steps/argument ### Description Specifies an argument for a Reactor step, defining its source and optional transformation. ### Method POST ### Endpoint /reactor/steps/argument ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **name** (atom) - Required - The name of the argument. - **source** (object) - Required - The source of the argument (e.g., input, result, value). - **type** (string) - Required - The type of source (e.g., 'input', 'result', 'value'). - **key** (string) - Required - The key or identifier for the source. - **path** (array) - Optional - Path to extract a specific value from the source. - **transform** (function) - Optional - A function to transform the argument's value. ### Request Example ```json { "name": "user_id", "source": { "type": "result", "key": "create_user", "path": ["id"] } } ``` ### Response #### Success Response (200) - **status** (string) - Indicates the argument was successfully defined. #### Response Example ```json { "status": "success" } ``` ``` -------------------------------- ### Integrate Telemetry for Reactor Events Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/data-pipelines.md Add the Telemetry middleware to a Reactor module to emit events for monitoring data pipelines. Includes an example of attaching a telemetry handler. ```elixir defmodule DataPipeline.UserETL do use Reactor middlewares do middleware Reactor.Middleware.Telemetry end # Steps... end # In your application :telemetry.attach("data-pipeline-handler", [:reactor, :step, :stop], &DataPipeline.Telemetry.handle_event/4, %{} ) ``` -------------------------------- ### Memory-Aware Batch Configuration for Lightweight Transformations Source: https://github.com/ash-project/reactor/blob/main/documentation/how-to/performance-optimization.md Configures a Reactor for lightweight transformations with small records. Uses a batch size of 1000, suitable for records around 1KB. ```elixir defmodule LightweightProcessor do use Reactor input :records map :process_records do source input(:records) batch_size 1000 # Safe for small records (~1KB each) step :transform_record do argument :record, element(:process_records) run fn %{record: record}, _context -> # Lightweight transformation {:ok, simple_transform(record)} end end return :transform_record end return :process_records end ``` -------------------------------- ### reactor.switch.default Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Defines the steps to be executed when none of the `matches?` conditions are met. This is an optional fallback. ```APIDOC ## POST reactor.switch.default ### Description Defines the steps to run if none of the `matches?` branches match the input. This is executed only if provided. ### Method POST ### Endpoint /reactor/switch/default ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **return** ( atom ) - Optional - Specify which step result to return upon completion. ### Request Example ```json { "return": "default_result_key" } ``` ### Response #### Success Response (200) - **status** (string) - The status of the operation. - **data** (any) - The result of the executed steps. #### Response Example ```json { "status": "success", "data": { "message": "Default steps executed." } } ``` ``` -------------------------------- ### reactor.template Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md Creates a step that renders an EEx template, passing provided arguments into the template context. It's useful for generating dynamic content. ```APIDOC ## POST reactor.template ### Description A step that passes all its arguments as assigns into an `EEx` template and returns the rendered result. ### Method POST ### Endpoint /reactor/template ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **name** ( atom ) - Required - A unique name for the step, used for return values and referencing. - **template** ( String.t ) - Required - An `EEx` template string. - **description** ( String.t ) - Optional - An optional description for the step. ### Request Example ```json { "name": "welcome_message", "template": "Welcome <%= @user.name %>! 🎉", "description": "Generates a welcome message." } ``` ### Response #### Success Response (200) - **rendered_content** (String.t) - The rendered output of the EEx template. #### Response Example ```json { "rendered_content": "Welcome John Doe! 🎉" } ``` ``` -------------------------------- ### Reactor DAG Construction with Dependencies Source: https://github.com/ash-project/reactor/blob/main/documentation/explanation/concepts.md Define steps and their dependencies using `input`, `result`, and `argument` macros. Dependencies are automatically created when step arguments reference results from other steps, forming a DAG. ```elixir defmodule UserRegistrationReactor do use Reactor input :email input :password step :validate_email do argument :email, input(:email) run fn %{email: email}, _context -> # Validation logic {:ok, validated_email} end end step :hash_password do argument :password, input(:password) # This step can run concurrently with :validate_email end step :create_user do argument :email, result(:validate_email) # Creates dependency edge argument :password, result(:hash_password) # Creates dependency edge # This step waits for both validate_email and hash_password end end ``` -------------------------------- ### Define a map argument from a result with transformation Source: https://github.com/ash-project/reactor/blob/main/documentation/dsls/DSL-Reactor.md This example defines a map argument sourced from a previous step's result, applying a transformation to extract a specific field ('id') before passing it as the argument. ```elixir argument :user_id, result(:create_user) do transform & &1.id end ```