AF
Ash Framework•4mo ago
Steve

Trouble Understanding Reactor

I'm taking a stab at implementing a Reactor, but the documentation isn't very clear. I've read both the Reactor documentation and the AshReactor documentation for getting started, but I'm still getting tripped up. Here is a basic example:
defmodule MyApp.Accounts.Reactors.RegisterUser do
@moduledoc false
use Ash.Reactor

alias MyApp.Accounts.User

input :email
input :name

create :create_user, User do
inputs %{name: input(:name), email: input(:email)}
end
end
defmodule MyApp.Accounts.Reactors.RegisterUser do
@moduledoc false
use Ash.Reactor

alias MyApp.Accounts.User

input :email
input :name

create :create_user, User do
inputs %{name: input(:name), email: input(:email)}
end
end
From what I gather, this will forward the inputs defined inside the create to the default create action of the User resource. Am I right? How would this differ if I had to call a different action that wasn't default. Next, I call this by placing an action on the User resource like so:
action :register_user do
argument :email, :ci_string, allow_nil?: false
argument :name, :string, allow_nil?: false

run RegisterUser
end
action :register_user do
argument :email, :ci_string, allow_nil?: false
argument :name, :string, allow_nil?: false

run RegisterUser
end
Calling User.register_user seems to run the RegisterUser reactor, which runs the User.create action like expected, but then I get this error:
** (Ash.Error.Framework.InvalidReturnType) Invalid return from generic action Elixir.MyApp.Accounts.User.register_user.

Expected :ok or {:error, error}, got:

%MyApp.Accounts.User{sessions: #Ash.NotLoaded<:relationship, field: :sessions>, __meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: "01976743-40a1-719d-b070-519808c3e38e", inserted_at: ~U[2025-06-13 03:09:10.177172Z], updated_at: ~U[2025-06-13 03:09:10.177172Z], email: #Ash.CiString<"test@example.com">, name: "Test User", role: :user, status: :pending}

(ash 3.5.12) lib/ash/actions/action.ex:211: Ash.Actions.Action.raise_invalid_generic_action_return!/2
(ash 3.5.12) lib/ash/actions/action.ex:143: Ash.Actions.Action.run/3
(ash 3.5.12) lib/ash.ex:1449: Ash.run_action/2
iex:16: (file)
** (Ash.Error.Framework.InvalidReturnType) Invalid return from generic action Elixir.MyApp.Accounts.User.register_user.

Expected :ok or {:error, error}, got:

%MyApp.Accounts.User{sessions: #Ash.NotLoaded<:relationship, field: :sessions>, __meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: "01976743-40a1-719d-b070-519808c3e38e", inserted_at: ~U[2025-06-13 03:09:10.177172Z], updated_at: ~U[2025-06-13 03:09:10.177172Z], email: #Ash.CiString<"test@example.com">, name: "Test User", role: :user, status: :pending}

(ash 3.5.12) lib/ash/actions/action.ex:211: Ash.Actions.Action.raise_invalid_generic_action_return!/2
(ash 3.5.12) lib/ash/actions/action.ex:143: Ash.Actions.Action.run/3
(ash 3.5.12) lib/ash.ex:1449: Ash.run_action/2
iex:16: (file)
Ok, so I then change my file to return just an :ok to see if I can get it to work. It looks like this:
defmodule MyApp.Accounts.Reactors.RegisterUser do
@moduledoc false
use Ash.Reactor

alias MyApp.Accounts.User

input :email
input :name

create :create_user, User do
inputs %{name: input(:name), email: input(:email)}
end

step :format_response do
wait_for :create_user

run fn _args, _context ->
:ok
end
end

return :format_response
end
defmodule MyApp.Accounts.Reactors.RegisterUser do
@moduledoc false
use Ash.Reactor

alias MyApp.Accounts.User

input :email
input :name

create :create_user, User do
inputs %{name: input(:name), email: input(:email)}
end

step :format_response do
wait_for :create_user

run fn _args, _context ->
:ok
end
end

return :format_response
end
I run User.register_user again and now I get this error (sorry for the formatting, this is how it's output): POST IS TOO LONG FOR DISCORD, THE REST IS IN THE FIRST COMMENT
35 Replies
Steve
SteveOP•4mo ago
{:error,
%Ash.Error.Unknown{
errors: [
%Ash.Error.Unknown.UnknownError{
error: "** (Reactor.Error.Invalid) \nInvalid Error\n\n* # Invalid Result Error\n\nThe step `:format_response` returned an invalid result.\n\nValid return types from the `c:Reactor.Step.run/3` callback are:\n\n- `{:ok, any}` - a successful result.\n- `{:ok, any, [Reactor.Step.t]}` - a successful result with additional steps to\n add to the running reactor.\n- `:retry` - the step wants to be retried.\n- `{:retry, Exception.t | any}` - the step wants to be retried, and here's why.\n- `{:error, Exception.t | any}` - the step failed, and here's why.\n- `{:halt, any}` - the step wants the Reactor to stop.\n\n## `result`:\n\n:ok\n\n## `step`:\n\n%Reactor.Step{arguments: [%Reactor.Argument{description: nil, name: :_, source: %Reactor.Template.Result{name: :create_user, sub_path: []}, transform: nil}], async?: true, context: %{}, description: nil, impl: {Reactor.Step.AnonFn, [run: &MyApp.Accounts.Reactors.RegisterUser.run_0_generated_307B2E0B8956B4480E9B0A1058AE42EE/2, compensate: nil, undo: nil]}, name: :format_response, max_retries: :infinity, ref: :format_response, transform: nil, guards: []}\n\n## `arguments`:\n\n%{}\n\n (reactor 0.15.3) lib/reactor/error/invalid/invalid_result_error.ex:6: Reactor.Error.Invalid.InvalidResultError.exception/1\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:210: Reactor.Executor.StepRunner.handle_run_result/5\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:161: Reactor.Executor.StepRunner.do_run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:51: Reactor.Executor.StepRunner.run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:70: Reactor.Executor.StepRunner.run_async/5\n (elixir 1.18.4) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2\n (elixir 1.18.4) lib/task/supervised.ex:36: Task.Supervised.reply/4",
field: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
]
}}
{:error,
%Ash.Error.Unknown{
errors: [
%Ash.Error.Unknown.UnknownError{
error: "** (Reactor.Error.Invalid) \nInvalid Error\n\n* # Invalid Result Error\n\nThe step `:format_response` returned an invalid result.\n\nValid return types from the `c:Reactor.Step.run/3` callback are:\n\n- `{:ok, any}` - a successful result.\n- `{:ok, any, [Reactor.Step.t]}` - a successful result with additional steps to\n add to the running reactor.\n- `:retry` - the step wants to be retried.\n- `{:retry, Exception.t | any}` - the step wants to be retried, and here's why.\n- `{:error, Exception.t | any}` - the step failed, and here's why.\n- `{:halt, any}` - the step wants the Reactor to stop.\n\n## `result`:\n\n:ok\n\n## `step`:\n\n%Reactor.Step{arguments: [%Reactor.Argument{description: nil, name: :_, source: %Reactor.Template.Result{name: :create_user, sub_path: []}, transform: nil}], async?: true, context: %{}, description: nil, impl: {Reactor.Step.AnonFn, [run: &MyApp.Accounts.Reactors.RegisterUser.run_0_generated_307B2E0B8956B4480E9B0A1058AE42EE/2, compensate: nil, undo: nil]}, name: :format_response, max_retries: :infinity, ref: :format_response, transform: nil, guards: []}\n\n## `arguments`:\n\n%{}\n\n (reactor 0.15.3) lib/reactor/error/invalid/invalid_result_error.ex:6: Reactor.Error.Invalid.InvalidResultError.exception/1\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:210: Reactor.Executor.StepRunner.handle_run_result/5\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:161: Reactor.Executor.StepRunner.do_run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:51: Reactor.Executor.StepRunner.run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:70: Reactor.Executor.StepRunner.run_async/5\n (elixir 1.18.4) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2\n (elixir 1.18.4) lib/task/supervised.ex:36: Task.Supervised.reply/4",
field: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
]
}}
Now it's saying that I need to return {:ok, any}, which I assume is what the call directly to the create action was doing. I look at the documentation pages and I don't understand what I'm doing wrong. Can anyone point me in the right direction? Reactors seem very useful and there's multiple steps to the registration process, but I can't even seem to get it to run one.
ZachDaniel
ZachDaniel•4mo ago
step :format_response do
wait_for :create_user

run fn _args, _context ->
:ok # <- needs to be {:ok, something}
end
end
step :format_response do
wait_for :create_user

run fn _args, _context ->
:ok # <- needs to be {:ok, something}
end
end
well, maybe not I'm actually not a wizkid at the bits and bobs of reactor but your issue w/ the generic action is because the generic action doesn't have a return type
Steve
SteveOP•4mo ago
Yeah, that results in the first error where it says it only expects an :ok
ZachDaniel
ZachDaniel•4mo ago
action :register_user, :struct do
constraints [instance_of: __MODULE__]
argument :email, :ci_string, allow_nil?: false
argument :name, :string, allow_nil?: false

run RegisterUser
end
action :register_user, :struct do
constraints [instance_of: __MODULE__]
argument :email, :ci_string, allow_nil?: false
argument :name, :string, allow_nil?: false

run RegisterUser
end
Steve
SteveOP•4mo ago
Is that instance_of what is expected as the return?
ZachDaniel
ZachDaniel•4mo ago
Generic action inputs and return types are explicit its the :struct type which has an instance_of constraint indicating what type of struct it is
Steve
SteveOP•4mo ago
Ah, I see now. Let me try it out really quick.
ZachDaniel
ZachDaniel•4mo ago
i.e
action :register_user, :struct do
...
end
action :register_user, :struct do
...
end
would be saying "it returns any struct" vs
action :register_user, :struct do
constraints [instance_of: __MODULE__]
end
action :register_user, :struct do
constraints [instance_of: __MODULE__]
end
is saying "it returns a struct which is an instance of __MODULE__"
Steve
SteveOP•4mo ago
Ah, yeah, adding that :struct at the top got it to work when creating a user. However, whenever there's an error returned during the create_user call, say due to validation errors or a unique constraint on email, it returns a big error that wraps the Invalid error, like so:
{:error,
%Ash.Error.Unknown{
errors: [
%Ash.Error.Unknown.UnknownError{
error: "** (Reactor.Error.Invalid) \nInvalid Error\n\n* # Run Step Error\n\nAn error occurred while attempting to run the `:create_user` step.\n\n## `step`:\n\n%Reactor.Step{arguments: [%Reactor.Argument{description: nil, name: :initial, source: %Reactor.Template.Value{value: MyApp.Accounts.User, sub_path: []}, transform: nil}, %Reactor.Argument{description: nil, name: :input, source: %Reactor.Template.Result{name: {:__input__, :create_user, [:name, :email]}, sub_path: []}, transform: nil}], async?: true, context: %{}, description: nil, impl: {Ash.Reactor.CreateStep, [domain: MyApp.Accounts, resource: MyApp.Accounts.User, action: :create, authorize?: nil, undo_action: nil, undo: :never, upsert_identity: nil, upsert?: false]}, name: :create_user, max_retries: 100, ref: :create_user, transform: nil, guards: []}\n\n## `error`:\n\n\nBread Crumbs:\n > Error returned from: MyApp.Accounts.User.create\n\nInvalid Error\n\n* Invalid value provided for email: must be a valid email address.\n\nnil\n\n (ash 3.5.12) lib/ash/error/changes/invalid_attribute.ex:4: Ash.Error.Changes.InvalidAttribute.exception/1\n (ash 3.5.12) lib/ash/changeset/changeset.ex:6327: Ash.Changeset.to_change_error/1\n (ash 3.5.12) lib/ash/changeset/changeset.ex:6273: Ash.Changeset.add_error/3\n (ash 3.5.12) lib/ash/changeset/changeset.ex:3422: Ash.Changeset.do_validation/5\n (elixir 1.18.4) lib/enum.ex:2546: Enum.\"-reduce/3-lists^foldl/2-0-\"/3\n (ash 3.5.12) lib/ash/changeset/changeset.ex:2174: Ash.Changeset.do_for_action/4\n (ash 3.5.12) lib/ash/reactor/steps/create_step.ex:43: Ash.Reactor.CreateStep.run/3\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:160: Reactor.Executor.StepRunner.do_run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:51: Reactor.Executor.StepRunner.run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:70: Reactor.Executor.StepRunner.run_async/5\n (elixir 1.18.4) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2\n (elixir 1.18.4) lib/task/supervised.ex:36: Task.Supervised.reply/4\n\n (reactor 0.15.3) lib/reactor/error/invalid/run_step_error.ex:8: Reactor.Error.Invalid.RunStepError.exception/1\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:196: Reactor.Executor.StepRunner.handle_run_result/5\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:161: Reactor.Executor.StepRunner.do_run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:51: Reactor.Executor.StepRunner.run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:70: Reactor.Executor.StepRunner.run_async/5\n (elixir 1.18.4) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2\n (elixir 1.18.4) lib/task/supervised.ex:36: Task.Supervised.reply/4",
field: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
]
}}
{:error,
%Ash.Error.Unknown{
errors: [
%Ash.Error.Unknown.UnknownError{
error: "** (Reactor.Error.Invalid) \nInvalid Error\n\n* # Run Step Error\n\nAn error occurred while attempting to run the `:create_user` step.\n\n## `step`:\n\n%Reactor.Step{arguments: [%Reactor.Argument{description: nil, name: :initial, source: %Reactor.Template.Value{value: MyApp.Accounts.User, sub_path: []}, transform: nil}, %Reactor.Argument{description: nil, name: :input, source: %Reactor.Template.Result{name: {:__input__, :create_user, [:name, :email]}, sub_path: []}, transform: nil}], async?: true, context: %{}, description: nil, impl: {Ash.Reactor.CreateStep, [domain: MyApp.Accounts, resource: MyApp.Accounts.User, action: :create, authorize?: nil, undo_action: nil, undo: :never, upsert_identity: nil, upsert?: false]}, name: :create_user, max_retries: 100, ref: :create_user, transform: nil, guards: []}\n\n## `error`:\n\n\nBread Crumbs:\n > Error returned from: MyApp.Accounts.User.create\n\nInvalid Error\n\n* Invalid value provided for email: must be a valid email address.\n\nnil\n\n (ash 3.5.12) lib/ash/error/changes/invalid_attribute.ex:4: Ash.Error.Changes.InvalidAttribute.exception/1\n (ash 3.5.12) lib/ash/changeset/changeset.ex:6327: Ash.Changeset.to_change_error/1\n (ash 3.5.12) lib/ash/changeset/changeset.ex:6273: Ash.Changeset.add_error/3\n (ash 3.5.12) lib/ash/changeset/changeset.ex:3422: Ash.Changeset.do_validation/5\n (elixir 1.18.4) lib/enum.ex:2546: Enum.\"-reduce/3-lists^foldl/2-0-\"/3\n (ash 3.5.12) lib/ash/changeset/changeset.ex:2174: Ash.Changeset.do_for_action/4\n (ash 3.5.12) lib/ash/reactor/steps/create_step.ex:43: Ash.Reactor.CreateStep.run/3\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:160: Reactor.Executor.StepRunner.do_run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:51: Reactor.Executor.StepRunner.run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:70: Reactor.Executor.StepRunner.run_async/5\n (elixir 1.18.4) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2\n (elixir 1.18.4) lib/task/supervised.ex:36: Task.Supervised.reply/4\n\n (reactor 0.15.3) lib/reactor/error/invalid/run_step_error.ex:8: Reactor.Error.Invalid.RunStepError.exception/1\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:196: Reactor.Executor.StepRunner.handle_run_result/5\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:161: Reactor.Executor.StepRunner.do_run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:51: Reactor.Executor.StepRunner.run/4\n (reactor 0.15.3) lib/reactor/executor/step_runner.ex:70: Reactor.Executor.StepRunner.run_async/5\n (elixir 1.18.4) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2\n (elixir 1.18.4) lib/task/supervised.ex:36: Task.Supervised.reply/4",
field: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
]
}}
Does this mean if I'm using Reactors to handle multiple steps/actions done during the registration process, that I lose the ability to report validation errors back to the caller?
ZachDaniel
ZachDaniel•4mo ago
🤔 ...that being an unknown error seems wrong to me @jart is this something reactor is doing or something wrong w/ this specific reactor?
jart
jart•4mo ago
Reactor has it's own splodes but they don't interact nicely with Ash's ones. I wish they did.
ZachDaniel
ZachDaniel•4mo ago
🤔 why don't they?
jart
jart•4mo ago
probably because I didn't (don't) really get how they're supposed to work with ash
ZachDaniel
ZachDaniel•4mo ago
They don't have to be automagically cast back to fields necessarily, we can leave that to the user but right now the setup is encoding the error as a string inside of an unknown error which is effectively useless It might be something wrong in splode
jart
jart•4mo ago
I think it might be because Reactor doesn't have all the error classes that Ash does
ZachDaniel
ZachDaniel•4mo ago
but we should add a test case for this and sort out how to get it to return an Ash.Error.Invalid containing reactors errors it should only be a problem if it was the other way around reactor having more classes than ash but, we can figure out where to add some custom glue or some kind of adapter something or other to fix any mismatch
jart
jart•4mo ago
yeah that makes sense. the current generic action -> reactor glue just maps up the run functions and nothing else but maybe reactor just shouldn't wrap ash errors?
ZachDaniel
ZachDaniel•4mo ago
oh, I see what you're saying
jart
jart•4mo ago
but then reactor has to account for multiple errors being possible in an undo
ZachDaniel
ZachDaniel•4mo ago
not really its always one error just could be the grouping of multiple errors
jart
jart•4mo ago
it's one error that triggers the rollback, but undo's can collect errors from failed undos
ZachDaniel
ZachDaniel•4mo ago
oh, I see what you're saying lemme just see what it looks like splode should solve it if the error classes match my vote is to just make sure that reactor has all the error classes of Ash, but even still, they both have :invalid so it shouldn't matter here. where would I find the code for handling a step error in reactor? anyway, some kind of protocol or adapter pattern would likely serve here in some way
jart
jart•4mo ago
all the errors live in Reactor.Error, but it will be StepRunner that actually records the errors
ZachDaniel
ZachDaniel•4mo ago
so in this code:
defp handle_undo(reactor, state, []) do
IO.inspect(state.errors)
error = Reactor.Error.to_class(state.errors)
IO.inspect(error)
Executor.Hooks.error(reactor, error, reactor.context)
end
defp handle_undo(reactor, state, []) do
IO.inspect(state.errors)
error = Reactor.Error.to_class(state.errors)
IO.inspect(error)
Executor.Hooks.error(reactor, error, reactor.context)
end
its playing nicely
jart
jart•4mo ago
look at step_runner.ex line 196
ZachDaniel
ZachDaniel•4mo ago
%Reactor.Error.Invalid{
errors: [
%Reactor.Error.Invalid.RunStepError{
error: %Ash.Error.Invalid{
errors: [
%Ash.Error.Changes.InvalidChanges{
fields: [:foo, :bar],
message: "foo",
validation: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
},
step: %Reactor.Step{
arguments: [
%Reactor.Argument{
description: nil,
name: :echo,
source: %Reactor.Template.Input{name: :input, sub_path: []},
transform: nil
}
],
async?: true,
context: %{},
description: nil,
impl: {Reactor.Step.AnonFn,
[
run: &Ash.Test.Actions.GenericActionsTest.ErrorReactor.run_0_generated_579F0ED6EFD8781F1AA1A3E09A2840A3/1,
compensate: nil,
undo: nil
]},
name: :echo,
max_retries: :infinity,
ref: :echo,
transform: nil,
guards: []
},
splode: Reactor.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
],
splode: Reactor.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
%Reactor.Error.Invalid{
errors: [
%Reactor.Error.Invalid.RunStepError{
error: %Ash.Error.Invalid{
errors: [
%Ash.Error.Changes.InvalidChanges{
fields: [:foo, :bar],
message: "foo",
validation: nil,
value: nil,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
},
step: %Reactor.Step{
arguments: [
%Reactor.Argument{
description: nil,
name: :echo,
source: %Reactor.Template.Input{name: :input, sub_path: []},
transform: nil
}
],
async?: true,
context: %{},
description: nil,
impl: {Reactor.Step.AnonFn,
[
run: &Ash.Test.Actions.GenericActionsTest.ErrorReactor.run_0_generated_579F0ED6EFD8781F1AA1A3E09A2840A3/1,
compensate: nil,
undo: nil
]},
name: :echo,
max_retries: :infinity,
ref: :echo,
transform: nil,
guards: []
},
splode: Reactor.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
],
splode: Reactor.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :unknown
}
Thats something you could theoretically do something with would be manual still like matching on a step error and returning it, but we could hide that away internally I think its Ash's error class stuff doing this Anyway, @Steve what you can do in the short term 🙂 (side note, I don't know how no one has mentioned this before, it seems like a really obvious thing, maybe people aren't using the automatic "run a reactor" option as much as we thought)
action :register_user do
argument :email, :ci_string, allow_nil?: false
argument :name, :string, allow_nil?: false

run fn input, _ ->
case Reactor.run(YourModule, input.arguments, %{}, []) do
{:ok, result} -> {:ok, result}
{:error, error} ->
# extract the Ash errors from a step error
{:error, those_errors}
end
end
end
action :register_user do
argument :email, :ci_string, allow_nil?: false
argument :name, :string, allow_nil?: false

run fn input, _ ->
case Reactor.run(YourModule, input.arguments, %{}, []) do
{:ok, result} -> {:ok, result}
{:error, error} ->
# extract the Ash errors from a step error
{:error, those_errors}
end
end
end
Steve
SteveOP•4mo ago
I didn't want to interrupt while y'all were talking. 😂 If I need something that interacts more nicely with Ash errors in the mean time, would I be able to do multi-step processes using just normal Ash? Is Reactor overkill for most multi-step processes?
jart
jart•4mo ago
https://hexdocs.pm/ash/multi-step-actions.html
Use reactor when: You need to compensate/undo changes across multiple external services Building complex workflows that require sophisticated error handling and rollback logic Coordinating long-running processes that span multiple systems
Steve
SteveOP•4mo ago
I'm looking through some of the more in-depth examples now, and it looks like they separate steps out into their own modules and return errors that way:
defmodule ECommerce.Steps.ReserveInventory do
use Reactor.Step

@impl true
def run(%{order: order}, _context, _options) do
case InventoryService.reserve_items(order.items) do
{:ok, reservation} -> {:ok, reservation}
{:error, :insufficient_stock} -> {:error, "Not enough inventory for order"}
{:error, reason} -> {:error, reason}
end
end

@impl true
def undo(reservation, _arguments, _context, _options) do
# Release the reserved inventory if later steps fail
case InventoryService.release_reservation(reservation.id) do
:ok -> :ok
{:error, :already_released} -> :ok # Already cleaned up
{:error, reason} -> {:error, reason}
end
end
end
defmodule ECommerce.Steps.ReserveInventory do
use Reactor.Step

@impl true
def run(%{order: order}, _context, _options) do
case InventoryService.reserve_items(order.items) do
{:ok, reservation} -> {:ok, reservation}
{:error, :insufficient_stock} -> {:error, "Not enough inventory for order"}
{:error, reason} -> {:error, reason}
end
end

@impl true
def undo(reservation, _arguments, _context, _options) do
# Release the reserved inventory if later steps fail
case InventoryService.release_reservation(reservation.id) do
:ok -> :ok
{:error, :already_released} -> :ok # Already cleaned up
{:error, reason} -> {:error, reason}
end
end
end
ZachDaniel
ZachDaniel•4mo ago
Yeah, taking it back to the basics I guess, why are you reaching for reactor 😄
Steve
SteveOP•4mo ago
I'm mostly experimenting right now. In this case, no particular reason other than to learn, which is why it was such a simple example. For registration, there's really only a few actions that need to happen after the user is created (creating some tokens, creating a couple new records for different resources, sending notifications). I suppose I should start with figuring out how to do it the normal Ash way 😅
ZachDaniel
ZachDaniel•4mo ago
Gotcha, yeah your best bet is likely to use hooks
jart
jart•4mo ago
agreed. but also we need to make Reactor play nicer with Ash
ZachDaniel
ZachDaniel•4mo ago
create :register do
change CreateSomeTokens
change CreateACoupleNewRecords
change SendNotifications
end
create :register do
change CreateSomeTokens
change CreateACoupleNewRecords
change SendNotifications
end
etc. the patterns around that are documented in the multi step actions guide that @jart linked to
Steve
SteveOP•4mo ago
All right, I'll play around with those and see if I can get it to work correctly. As always, I really appreciate the help!

Did you find this page helpful?