How to use `after_action` when soft delete is enabled in global `Change`

Hi, Sorry! I have a global change that i am using inside my many resources! the problem i have when i use soft delete Ash.destroy!(Ash.get!(MishkaCms.Runtime.Site, id)) it is not triggerd It does not work
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
Ash.Changeset.after_action(changeset, fn changeset, record ->
IO.inspect(changeset, label: "changeset==========")
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
Ash.Changeset.after_action(changeset, fn changeset, record ->
IO.inspect(changeset, label: "changeset==========")
But it works
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")
Ash.Changeset.after_action(changeset, fn changeset, record ->
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")
Ash.Changeset.after_action(changeset, fn changeset, record ->
Is it way to use Archive destroy inside Ash.Changeset.after_action ? By the way my config in my resource
postgres do
table "sites"
repo MishkaCms.Repo

# Database-level optimization for archived records
base_filter_sql "(archived_at IS NULL)"
...

archive do
# base_filter? false
exclude_read_actions [:archived]
exclude_destroy_actions [:permanent_destroy]
end

changes do
change {Changes.SendToOban, []}, on: [:create, :update, :destroy]
end
postgres do
table "sites"
repo MishkaCms.Repo

# Database-level optimization for archived records
base_filter_sql "(archived_at IS NULL)"
...

archive do
# base_filter? false
exclude_read_actions [:archived]
exclude_destroy_actions [:permanent_destroy]
end

changes do
change {Changes.SendToOban, []}, on: [:create, :update, :destroy]
end
I have read this but i am using global Change - https://hexdocs.pm/ash_archival/0.1.2/archival.html Thank you in advance
Solution:
So it could be that it uses the atomic callback depending on how you call your action and your atomic implementation isn't doing anything. But as your change only has an after_action and that's allowed in the context of atomics you can try this: ```elixir defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do use Ash.Resource.Change...
Jump to solution
11 Replies
Rebecca Le
Rebecca Le3mo ago
global changes don't run on destroy by default - you have to opt in if you want them to do so https://hexdocs.pm/ash/dsl-ash-resource.html#changes-change eg. change MyChange, on: [:destroy]
Shahryar
ShahryarOP3mo ago
Hi @Rebecca Le , Global change works i can see the print as i say! the Ash.Changeset.after_action dose not run! for example it works
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")
Ash.Changeset.after_action(changeset, fn changeset, record ->
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")
Ash.Changeset.after_action(changeset, fn changeset, record ->
Rebecca Le
Rebecca Le3mo ago
gotcha, I misunderstood the issue, my apologies is that the version of AshArchival you're using, 0.1.2? that's quite old, can you try upgrading?
Shahryar
ShahryarOP3mo ago
i am using
:ash_archival, "2.0.1"
:ash_archival, "2.0.1"
the last version
barnabasj
barnabasj3mo ago
Just to make sure, are you returning the modified changeset from your change?
Shahryar
ShahryarOP3mo ago
No i just send data to my worker, simple change
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")

Ash.Changeset.after_action(changeset, fn changeset, record ->
if changeset.action.type != :update or changeset.context[:changed?] do
enqueue_job(changeset, record)
end

{:ok, record}
end)
end

@impl true
def atomic(_changeset, _opts, _context) do
{:atomic, %{}}
end

if Code.ensure_loaded?(Mix) and Mix.env() == :test do
defp enqueue_job(_changeset, _record) do
:ok
end
else
defp enqueue_job(changeset, record) do
original_record = changeset.data

# Get the old values with defaults
old_data = %{
active: Map.get(original_record, :active, false),
precompile: Map.get(original_record, :precompile, false)
}

%{
resource: inspect(changeset.resource),
action: changeset.action.type,
data:
Ash.Resource.Info.attributes(changeset.resource)
|> Enum.map(fn attr -> {attr.name, Map.get(record, attr.name)} end)
|> Map.new(),
old_data: old_data
}
|> MishkaCms.Runtime.Workers.RuntimeCompilerWorker.new()
|> Oban.insert()
end
end
end
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")

Ash.Changeset.after_action(changeset, fn changeset, record ->
if changeset.action.type != :update or changeset.context[:changed?] do
enqueue_job(changeset, record)
end

{:ok, record}
end)
end

@impl true
def atomic(_changeset, _opts, _context) do
{:atomic, %{}}
end

if Code.ensure_loaded?(Mix) and Mix.env() == :test do
defp enqueue_job(_changeset, _record) do
:ok
end
else
defp enqueue_job(changeset, record) do
original_record = changeset.data

# Get the old values with defaults
old_data = %{
active: Map.get(original_record, :active, false),
precompile: Map.get(original_record, :precompile, false)
}

%{
resource: inspect(changeset.resource),
action: changeset.action.type,
data:
Ash.Resource.Info.attributes(changeset.resource)
|> Enum.map(fn attr -> {attr.name, Map.get(record, attr.name)} end)
|> Map.new(),
old_data: old_data
}
|> MishkaCms.Runtime.Workers.RuntimeCompilerWorker.new()
|> Oban.insert()
end
end
end
Solution
barnabasj
barnabasj3mo ago
So it could be that it uses the atomic callback depending on how you call your action and your atomic implementation isn't doing anything. But as your change only has an after_action and that's allowed in the context of atomics you can try this:
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")

Ash.Changeset.after_action(changeset, fn changeset, record ->
if changeset.action.type != :update or changeset.context[:changed?] do
enqueue_job(changeset, record)
end

{:ok, record}
end)
end

# UPDATED this function
@impl true
def atomic(changeset, opts, context) do
{:ok, change(changeset, opts, context)}
end

if Code.ensure_loaded?(Mix) and Mix.env() == :test do
defp enqueue_job(_changeset, _record) do
:ok
end
else
defp enqueue_job(changeset, record) do
original_record = changeset.data

# Get the old values with defaults
old_data = %{
active: Map.get(original_record, :active, false),
precompile: Map.get(original_record, :precompile, false)
}

%{
resource: inspect(changeset.resource),
action: changeset.action.type,
data:
Ash.Resource.Info.attributes(changeset.resource)
|> Enum.map(fn attr -> {attr.name, Map.get(record, attr.name)} end)
|> Map.new(),
old_data: old_data
}
|> MishkaCms.Runtime.Workers.RuntimeCompilerWorker.new()
|> Oban.insert()
end
end
end
defmodule MishkaCms.Runtime.Resources.Changes.SendToOban do
use Ash.Resource.Change

@impl true
def change(changeset, _opts, _context) do
IO.inspect(changeset, label: "changeset==========")

Ash.Changeset.after_action(changeset, fn changeset, record ->
if changeset.action.type != :update or changeset.context[:changed?] do
enqueue_job(changeset, record)
end

{:ok, record}
end)
end

# UPDATED this function
@impl true
def atomic(changeset, opts, context) do
{:ok, change(changeset, opts, context)}
end

if Code.ensure_loaded?(Mix) and Mix.env() == :test do
defp enqueue_job(_changeset, _record) do
:ok
end
else
defp enqueue_job(changeset, record) do
original_record = changeset.data

# Get the old values with defaults
old_data = %{
active: Map.get(original_record, :active, false),
precompile: Map.get(original_record, :precompile, false)
}

%{
resource: inspect(changeset.resource),
action: changeset.action.type,
data:
Ash.Resource.Info.attributes(changeset.resource)
|> Enum.map(fn attr -> {attr.name, Map.get(record, attr.name)} end)
|> Map.new(),
old_data: old_data
}
|> MishkaCms.Runtime.Workers.RuntimeCompilerWorker.new()
|> Oban.insert()
end
end
end
Shahryar
ShahryarOP3mo ago
Ammm, but it has error
** (Ash.Error.Unknown)
Bread Crumbs:
> Exception raised in: MishkaCms.Runtime.Site.destroy

Unknown Error

* ** (Protocol.UndefinedError) protocol Enumerable not implemented for type Ash.Changeset (a struct)

Got value:

#Ash.Changeset<
domain: MishkaCms.Runtime,
action_type: :destroy,
action: :destroy,
attributes: %{},
atomics: [archived_at: ~U[2025-08-04 07:37:18.235361Z]],
relationships: %{},
errors: [],
data: %MishkaCms.Runtime.Site{
id: "a1b1385e-04a5-4291-ac02-64b4a0b61067",
name: "site3",
host: "localhost3",
priority: 0,
active: true,
inserted_at: ~U[2025-08-03 15:30:24.235227Z],
updated_at: ~U[2025-08-03 15:30:24.235227Z],
archived_at: nil,
__meta__: #Ecto.Schema.Metadata<:loaded, "sites">
},
valid?: true
>

(elixir 1.18.4) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.18.4) lib/enum.ex:166: Enumerable.reduce/3
(elixir 1.18.4) lib/enum.ex:4515: Enum.reduce/3
(ash 3.5.32) lib/ash/changeset/changeset.ex:1216: Ash.Changeset.run_atomic_change/3
(ash 3.5.32) lib/ash/changeset/changeset.ex:1043: anonymous fn/3 in Ash.Changeset.atomic_changes/2
(elixir 1.18.4) lib/enum.ex:4968: Enumerable.List.reduce/3
(elixir 1.18.4) lib/enum.ex:2600: Enum.reduce_while/3
(ash 3.5.32) lib/ash/changeset/changeset.ex:1041: Ash.Changeset.atomic_changes/2
(ash 3.5.32) lib/ash/changeset/changeset.ex:833: Ash.Changeset.fully_atomic_changeset/4
(ash 3.5.32) lib/ash/actions/update/update.ex:88: Ash.Actions.Update.run/4
(ash 3.5.32) lib/ash.ex:3771: Ash.destroy/2
(ash 3.5.32) lib/ash.ex:3700: Ash.destroy!/2
(elixir 1.18.4) src/elixir.erl:386: :elixir.eval_external_handler/3
(stdlib 7.0.2) erl_eval.erl:924: :erl_eval.do_apply/7
(elixir 1.18.4) src/elixir.erl:364: :elixir.eval_forms/4
(elixir 1.18.4) lib/module/parallel_checker.ex:120: Module.ParallelChecker.verify/1
...
** (Ash.Error.Unknown)
Bread Crumbs:
> Exception raised in: MishkaCms.Runtime.Site.destroy

Unknown Error

* ** (Protocol.UndefinedError) protocol Enumerable not implemented for type Ash.Changeset (a struct)

Got value:

#Ash.Changeset<
domain: MishkaCms.Runtime,
action_type: :destroy,
action: :destroy,
attributes: %{},
atomics: [archived_at: ~U[2025-08-04 07:37:18.235361Z]],
relationships: %{},
errors: [],
data: %MishkaCms.Runtime.Site{
id: "a1b1385e-04a5-4291-ac02-64b4a0b61067",
name: "site3",
host: "localhost3",
priority: 0,
active: true,
inserted_at: ~U[2025-08-03 15:30:24.235227Z],
updated_at: ~U[2025-08-03 15:30:24.235227Z],
archived_at: nil,
__meta__: #Ecto.Schema.Metadata<:loaded, "sites">
},
valid?: true
>

(elixir 1.18.4) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.18.4) lib/enum.ex:166: Enumerable.reduce/3
(elixir 1.18.4) lib/enum.ex:4515: Enum.reduce/3
(ash 3.5.32) lib/ash/changeset/changeset.ex:1216: Ash.Changeset.run_atomic_change/3
(ash 3.5.32) lib/ash/changeset/changeset.ex:1043: anonymous fn/3 in Ash.Changeset.atomic_changes/2
(elixir 1.18.4) lib/enum.ex:4968: Enumerable.List.reduce/3
(elixir 1.18.4) lib/enum.ex:2600: Enum.reduce_while/3
(ash 3.5.32) lib/ash/changeset/changeset.ex:1041: Ash.Changeset.atomic_changes/2
(ash 3.5.32) lib/ash/changeset/changeset.ex:833: Ash.Changeset.fully_atomic_changeset/4
(ash 3.5.32) lib/ash/actions/update/update.ex:88: Ash.Actions.Update.run/4
(ash 3.5.32) lib/ash.ex:3771: Ash.destroy/2
(ash 3.5.32) lib/ash.ex:3700: Ash.destroy!/2
(elixir 1.18.4) src/elixir.erl:386: :elixir.eval_external_handler/3
(stdlib 7.0.2) erl_eval.erl:924: :erl_eval.do_apply/7
(elixir 1.18.4) src/elixir.erl:364: :elixir.eval_forms/4
(elixir 1.18.4) lib/module/parallel_checker.ex:120: Module.ParallelChecker.verify/1
...
without chaning atomic before after_action
#Ash.Changeset<
domain: MishkaCms.Runtime,
action_type: :destroy,
action: :destroy,
attributes: %{archived_at: ~U[2025-08-04 07:39:18.678884Z]},
relationships: %{},
errors: [],
data: %MishkaCms.Runtime.Site{
id: "a1b1385e-04a5-4291-ac02-64b4a0b61067",
name: "site3",
host: "localhost3",
priority: 0,
active: true,
inserted_at: ~U[2025-08-03 15:30:24.235227Z],
updated_at: ~U[2025-08-03 15:30:24.235227Z],
archived_at: nil,
__meta__: #Ecto.Schema.Metadata<:loaded, "sites">
},
valid?: true
>
#Ash.Changeset<
domain: MishkaCms.Runtime,
action_type: :destroy,
action: :destroy,
attributes: %{archived_at: ~U[2025-08-04 07:39:18.678884Z]},
relationships: %{},
errors: [],
data: %MishkaCms.Runtime.Site{
id: "a1b1385e-04a5-4291-ac02-64b4a0b61067",
name: "site3",
host: "localhost3",
priority: 0,
active: true,
inserted_at: ~U[2025-08-03 15:30:24.235227Z],
updated_at: ~U[2025-08-03 15:30:24.235227Z],
archived_at: nil,
__meta__: #Ecto.Schema.Metadata<:loaded, "sites">
},
valid?: true
>
barnabasj
barnabasj3mo ago
ah, yeah it should return {:ok, changeset} not {:atomic, changeset} I updated the example above
Shahryar
ShahryarOP3mo ago
Thank you it works, sorry to waste your time
barnabasj
barnabasj3mo ago
All good, no time wasted. The next person finding this thread will be glad someone asked.

Did you find this page helpful?