I have two resources, Teams and Users, and these are related using the relationship resource Members. I’m trying to use manage_relationship rather than write a bunch of code.
Adding team members to a team worked “out of the box,” as expected with no problems. I’ve been stuck on deleting team members from a team for over a day… I’ve simplified the case a good bit, and am completely stuck. (I’ve been through the Ash docs, examples, and Ash Framework book looking for the solution… no luck).
Here’s the Members resource:
defmodule WasteWalk.Teams.Members do
use Ash.Resource,
otp_app: :waste_walk,
domain: WasteWalk.Teams,
data_layer: AshPostgres.DataLayer
postgres do
table "team_members"
repo WasteWalk.Repo
references do
reference :user, on_delete: :delete, index?: true
reference :team, on_delete: :delete
end
end
actions do
create :create, accept: [:user_id, :team_id], primary?: true
destroy :destroy, accept: [:user_id, :team_id], primary?: true
end
relationships do
belongs_to :user, WasteWalk.Accounts.User, primary_key?: true, allow_nil?: false
belongs_to :team, WasteWalk.Teams.Team, primary_key?: true, allow_nil?: false
actions do
defaults [:read, update: :*]
end
end
end
On the Team resource, here’s the relevant functions for adding and deleting team members:
defmodule WasteWalk.Teams.Team do
...
actions do
defaults [:read]
create :create do
accept [:name]
end
update :update do
accept [:name]
end
update :add_team_member do
argument :user_id, :uuid, allow_nil?: false
require_atomic? false
change manage_relationship(:user_id, :team_memberships, value_is_key: :user_id, type: :create)
end
destroy :remove_team_member do
argument :user_id, :uuid, allow_nil?: false
require_atomic? false
# change manage_relationship(:user_id, :team_memberships, value_is_key: :user_id, type: :remove)
# change manage_relationship(:user_id, :team_memberships, type: :append_and_remove)
change manage_relationship(:user_id, :team_memberships, type: :remove)
end
end
relationships do
has_many :team_memberships, WasteWalk.Teams.Members
has_many :sprints, WasteWalk.Sprints.Sprint
many_to_many :team_members, WasteWalk.Accounts.User do
join_relationship :team_memberships
source_attribute_on_join_resource :team_id
destination_attribute_on_join_resource :user_id
end
end
Note that in my first attempt, instead of destroy :remove_team_member I started with update :remove_team_member. This seemed to make sense since I’m not destroying the team, I’m just updating one of its relationships. In that case the first commented-out line (with value_is_key:) compiled, but ultimately resulted in:
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Unknown{path: [:user_id, 0], errors: [%Ash.Error.Unknown.UnknownError{error: "** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column \"team_id\" of relation \"team_members\" violates not-null constraint...
Which, in turn quickly led me down the path of using destroy (to delete the row defining the Members relationship). But that doesn’t work either… (see below).
I’ve written a a suite of tests – everything passing (creating teams, adding members, queries, etc). The only thing that’s failing is removing team members from a team.
In the :remove_team_member function you can see a few variations that I’ve tried (I’ve tried a lot of things here). The one that seems most straight-forward and obvious is currently uncommented, change manage_relationship(:user_id, :team_memberships, type: :remove).
Here’s the relevant test:
test "only allow team leads to manage membership", context do
{:ok, team} = WasteWalk.Teams.new_team("A team", actor: context.admin)
{:ok, team} = WasteWalk.Teams.add_team_member(team.id, context.new_team_member.id, actor: context.admin)
{:ok, team} = WasteWalk.Teams.add_team_member(team.id, context.new_team_lead.id, actor: context.admin)
team = Ash.load!(team, :team_memberships, actor: context.new_team_lead)
team |> IO.inspect(label: "team after adding new_team_member")
assert length(team.team_memberships) == 2
{:ok, _} = WasteWalk.Teams.remove_team_member(team.id, context.new_team_member.id, actor: context.admin)
|> IO.inspect(label: "remove_team_member call")
end
The test fails, returning:
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidRelationship{relationship: :team_memberships, message: "changes would create a new related record", splode: Ash.Error, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid}]}}
Oddly, if I change the manage_relationship call to use :append_and_remove I get this error:
** (MatchError) no match of right hand side value: {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{primary_key: "1758e517-8a4e-40d8-89ac-2a3e9df9f860", resource: WasteWalk.Teams.Members, splode: Ash.Error, bread_crumbs: [], vars: [], path: [:user_id, 0], stacktrace: #Splode.Stacktrace<>, class: :invalid}]}}
Here’s the full test output:
team after adding new_team_member: %WasteWalk.Teams.Team{
id: "f9ab360a-94c4-41fb-a4fd-971a50ea458a",
name: "Team with Lead",
inserted_at: ~U[2025-08-12 07:50:38.774100Z],
updated_at: ~U[2025-08-12 07:50:38.774100Z],
is_member: #Ash.NotLoaded<:calculation, field: :is_member>,
team_memberships: [
%WasteWalk.Teams.Members{
user_id: "f3464150-0cb5-424e-a2a8-5bf822b60d16",
team_id: "f9ab360a-94c4-41fb-a4fd-971a50ea458a",
user: #Ash.NotLoaded<:relationship, field: :user>,
team: #Ash.NotLoaded<:relationship, field: :team>,
__meta__: #Ecto.Schema.Metadata<:loaded, "team_members">
},
%WasteWalk.Teams.Members{
user_id: "1758e517-8a4e-40d8-89ac-2a3e9df9f860",
team_id: "f9ab360a-94c4-41fb-a4fd-971a50ea458a",
user: #Ash.NotLoaded<:relationship, field: :user>,
team: #Ash.NotLoaded<:relationship, field: :team>,
__meta__: #Ecto.Schema.Metadata<:loaded, "team_members">
}
],
sprints: #Ash.NotLoaded<:relationship, field: :sprints>,
team_members: #Ash.NotLoaded<:relationship, field: :team_members>,
__meta__: #Ecto.Schema.Metadata<:loaded, "teams">
}
remove_team_member call: {:error,
%Ash.Error.Invalid{
errors: [
%Ash.Error.Query.NotFound{
primary_key: "1758e517-8a4e-40d8-89ac-2a3e9df9f860",
resource: WasteWalk.Teams.Members,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [:user_id, 0],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
}}






















