I have a resource called Meeting which has a many to many relationship. My create action looks like this:
defmodule Meeting do
create :create do
accept [:date]
change manage_relationship(
:instances,
type: :append_and_remove
)
end
when I create a meeting through iex this works just fine:
MyApp.Meetings.create_meeting(%{date: ~D[2020-01-01], instances: [%{id: 16}]}, actor: u)
# which produces
{:ok,
%MyApp.Meetings.Meeting{
id: "33be6aed-3e72-475f-bcdd-5da3749722ad",
date: ~D[2020-01-01],
inserted_at: ~U[2025-11-15 21:10:33.992514Z],
updated_at: ~U[2025-11-15 21:10:33.992514Z],
is_upcoming?: #Ash.NotLoaded<:calculation, field: :is_upcoming?>,
actor_is_part_of_meeting?: #Ash.NotLoaded<:calculation, field: :actor_is_part_of_meeting?>,
users: #Ash.NotLoaded<:relationship, field: :users>,
instances: [
%MyApp.Core.Instance{
id: 16,
dossier_number: #Ash.NotLoaded<:calculation, field: :dossier_number>,
__meta__: #Ecto.Schema.Metadata<:loaded, "INSTANCE">
}
],
users_join_assoc: #Ash.NotLoaded<:relationship, field: :users_join_assoc>,
instances_join_assoc: #Ash.NotLoaded<:relationship, field: :instances_join_assoc>,
__meta__: #Ecto.Schema.Metadata<:loaded, "meetings">
}}
However, when I run the same thing in a liveview:
<.form for={@form} class="uk-form-stacked" phx-submit="submit" phx-change="validate">
<.input field={@form[:date]} label="Datum" type="date" type="date" class="uk-input" />
<div class="uk-flex">
<div class="uk-flex-1-2">
<.inputs_for :let={instance} field={@form[:instances]}>
<!-- <.input field={instance[:dossier_number]} /> -->
<.input field={instance[:id]} />
</.inputs_for>
<label>
<input
type="checkbox"
name={"#{@form.name}[_add_instances]"}
value="end"
class="hidden"
/>
<span class="uk-button uk-button-secondary">
Neues Dossier hinzufügen
</span>
</label>
-->
</div>
</div>
<button type="submit" class="uk-button uk-button-primary uk-margin">Speichern</button>
</.form>
with some very vanilla event handlers:
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
form = MyApp.Meetings.form_to_create_meeting(actor: socket.assigns.current_user)
socket = assign(socket, :form, to_form(form))
{:ok, socket}
end
@impl Phoenix.LiveView
def handle_event("submit", %{"form" => form}, socket) do
dbg(form)
case AshPhoenix.Form.submit(socket.assigns.form,
params: form,
actor: socket.assigns.current_user
) do
{:ok, _meeting} ->
{:noreply, push_navigate(socket, to: ~p"/meetings")}
{:error, form} ->
dbg(form)
{:noreply, assign(socket, :form, form)}
end
end
@impl Phoenix.LiveView
def handle_event("validate", %{"form" => form}, socket) do
{:noreply,
assign(
socket,
:form,
socket.assigns.form
|> AshPhoenix.Form.validate(form, actor: socket.assigns.current_user)
|> to_form()
)}
end
I get this (it errors out during the creation of the join resource)
[(my_app 0.1.0) lib/my_app_web/live/meetings_new_live.ex:70: MyAppWeb.MeetingsNewLive.handle_event/3]
form #=> %{
"date" => "1111-01-11",
"instances" => %{
"0" => %{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
}
}
}
[(my_app 0.1.0) lib/my_app_web/live/meetings_new_live.ex:80: MyAppWeb.MeetingsNewLive.handle_event/3]
form #=> %Phoenix.HTML.Form{
source: #AshPhoenix.Form<
resource: MyApp.Meetings.Meeting,
action: :create,
type: :create,
params: %{
"date" => "1111-01-11",
"instances" => %{
"0" => %{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
}
}
},
source: #Ash.Changeset<
domain: MyApp.Meetings,
action_type: :create,
action: :create,
attributes: %{date: ~D[1111-01-11]},
relationships: %{
instances: [
{[
%{
dossier_number: %{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
}
}
],
[
debug?: false,
ignore?: false,
on_missing: :unrelate,
on_match: :ignore,
on_lookup: {:relate, :create, :read, [:dossier_number]},
on_no_match: :error,
eager_validate_with: false,
authorize?: true,
meta: [id: :instances],
type: :append_and_remove,
use_identities: [:dossier_number],
value_is_key: :dossier_number
]}
]
},
arguments: %{
instances: [
%{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
}
]
},
errors: [],
data: %MyApp.Meetings.Meeting{
id: nil,
date: nil,
inserted_at: nil,
updated_at: nil,
is_upcoming?: #Ash.NotLoaded<:calculation, field: :is_upcoming?>,
actor_is_part_of_meeting?: #Ash.NotLoaded<:calculation, field: :actor_is_part_of_meeting?>,
users: #Ash.NotLoaded<:relationship, field: :users>,
instances: #Ash.NotLoaded<:relationship, field: :instances>,
users_join_assoc: #Ash.NotLoaded<:relationship, field: :users_join_assoc>,
instances_join_assoc: #Ash.NotLoaded<:relationship, field: :instances_join_assoc>,
__meta__: #Ecto.Schema.Metadata<:built, "meetings">
},
valid?: true
>,
name: "form",
data: nil,
form_keys: [
instances: [
data: #Function<25.61500058/1 in AshPhoenix.Form.Auto.relationship_fetcher/3>,
read_action: :read,
read_resource: MyApp.Core.Instance,
type: :list,
forms: [
_update: [
resource: MyApp.Meetings.MeetingInstance,
managed_relationship: {MyApp.Meetings.Meeting, :instances,
[
on_missing: {:unrelate, :destroy},
type: :append_and_remove,
on_lookup: {:relate, :create, :read, [:dossier_number]},
on_no_match: :error,
on_match: :ignore,
use_identities: [:dossier_number],
value_is_key: :dossier_number
]},
type: :single,
data: %MyApp.Meetings.MeetingInstance{
id: nil,
inserted_at: nil,
updated_at: nil,
instance_id: nil,
meeting_id: nil,
instance: #Ash.NotLoaded<:relationship, field: :instance>,
meeting: #Ash.NotLoaded<:relationship, field: :meeting>,
__meta__: #Ecto.Schema.Metadata<:built, "meeting_instances">
},
update_action: :create
],
_join: [
resource: MyApp.Meetings.MeetingInstance,
managed_relationship: {MyApp.Meetings.Meeting, :instances,
[
on_missing: {:unrelate, :destroy},
type: :append_and_remove,
on_lookup: {:relate, :create, :read, [:dossier_number]},
on_no_match: :error,
on_match: :ignore,
use_identities: [:dossier_number],
value_is_key: :dossier_number
]},
type: :single,
data: #Function<44.61500058/2 in AshPhoenix.Form.Auto.add_join_form/4>,
destroy_fields: [],
destroy_action: :destroy,
merge?: true
]
],
sparse?: false,
managed_relationship: {MyApp.Meetings.Meeting, :instances,
[
on_missing: {:unrelate, :destroy},
type: :append_and_remove,
on_lookup: {:relate, :create, :read, [:dossier_number]},
on_no_match: :error,
on_match: :ignore,
use_identities: [:dossier_number],
value_is_key: :dossier_number
]},
must_load?: false
]
],
forms: %{
instances: [
#AshPhoenix.Form<
resource: MyApp.Core.Instance,
action: :read,
type: :read,
params: %{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
},
source: #Ash.Query<resource: MyApp.Core.Instance, action: :read>,
name: "form[instances][0]",
data: nil,
form_keys: [
_update: [
resource: MyApp.Meetings.MeetingInstance,
managed_relationship: {MyApp.Meetings.Meeting, :instances,
[
on_missing: {:unrelate, :destroy},
type: :append_and_remove,
on_lookup: {:relate, :create, :read, [:dossier_number]},
on_no_match: :error,
on_match: :ignore,
use_identities: [:dossier_number],
value_is_key: :dossier_number
]},
type: :single,
data: %MyApp.Meetings.MeetingInstance{
id: nil,
inserted_at: nil,
updated_at: nil,
instance_id: nil,
meeting_id: nil,
instance: #Ash.NotLoaded<:relationship, field: :instance>,
meeting: #Ash.NotLoaded<:relationship, field: :meeting>,
__meta__: #Ecto.Schema.Metadata<:built, "meeting_instances">
},
update_action: :create
],
_join: [
resource: MyApp.Meetings.MeetingInstance,
managed_relationship: {MyApp.Meetings.Meeting, :instances,
[
on_missing: {:unrelate, :destroy},
type: :append_and_remove,
on_lookup: {:relate, :create, :read, [:dossier_number]},
on_no_match: :error,
on_match: :ignore,
use_identities: [:dossier_number],
value_is_key: :dossier_number
]},
type: :single,
data: #Function<44.61500058/2 in AshPhoenix.Form.Auto.add_join_form/4>,
destroy_fields: [],
destroy_action: :destroy,
merge?: true
]
],
forms: %{
_update: #AshPhoenix.Form<
resource: MyApp.Meetings.MeetingInstance,
action: :create,
type: :create,
params: %{},
source: #Ash.Changeset<
domain: MyApp.Meetings,
action_type: :create,
action: :create,
attributes: %{},
relationships: %{},
errors: [
%Ash.Error.Changes.Required{
field: :meeting_id,
type: :attribute,
resource: MyApp.Meetings.MeetingInstance,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
},
%Ash.Error.Changes.Required{
field: :instance_id,
type: :attribute,
resource: MyApp.Meetings.MeetingInstance,
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
],
data: %MyApp.Meetings.MeetingInstance{
id: nil,
inserted_at: nil,
updated_at: nil,
instance_id: nil,
meeting_id: nil,
instance: #Ash.NotLoaded<:relationship, field: :instance>,
meeting: #Ash.NotLoaded<:relationship, field: :meeting>,
__meta__: #Ecto.Schema.Metadata<:built, "meeting_instances">
},
context: %{
accessing_from: %{
name: :instances,
source: MyApp.Meetings.Meeting,
manage_relationship_opts: [
on_missing: {:unrelate, :destroy},
type: :append_and_remove,
on_lookup: {:relate, :create, :read, [:dossier_number]},
on_no_match: :error,
on_match: :ignore,
use_identities: [:dossier_number],
value_is_key: :dossier_number
]
}
},
valid?: false
>,
name: "form[instances][0][_update]",
data: nil,
form_keys: [],
forms: %{},
domain: MyApp.Meetings,
method: "post",
submit_errors: [
meeting_id: {"is required", []},
instance_id: {"is required", []}
],
id: "form_instances_0__update",
transform_errors: nil,
post_process_errors: nil,
original_data: nil,
transform_params: nil,
prepare_params: nil,
prepare_source: nil,
raw_params: %{},
warn_on_unhandled_errors?: true,
any_removed?: false,
added?: false,
changed?: false,
touched_forms: MapSet.new([]),
valid?: false,
errors: true,
submitted_once?: true,
...
>
},
domain: MyApp.Core,
method: "post",
submit_errors: [],
id: "form_instances_0",
transform_errors: nil,
post_process_errors: nil,
original_data: nil,
transform_params: nil,
prepare_params: nil,
prepare_source: nil,
raw_params: %{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
},
warn_on_unhandled_errors?: true,
any_removed?: false,
added?: false,
changed?: false,
touched_forms: MapSet.new(["_form_type", "_persistent_id", "_touched",
"dossier_number"]),
valid?: false,
errors: true,
submitted_once?: true,
just_submitted?: true,
...
>
]
},
domain: MyApp.Meetings,
method: "post",
submit_errors: [],
id: "form",
transform_errors: nil,
post_process_errors: nil,
original_data: nil,
transform_params: nil,
prepare_params: nil,
prepare_source: nil,
raw_params: %{
"date" => "1111-01-11",
"instances" => %{
"0" => %{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
}
}
},
warn_on_unhandled_errors?: true,
any_removed?: false,
added?: false,
changed?: true,
touched_forms: MapSet.new(["date", "instances"]),
valid?: false,
errors: true,
submitted_once?: true,
just_submitted?: true,
...
>,
impl: Phoenix.HTML.FormData.AshPhoenix.Form,
id: "form",
name: "form",
data: nil,
action: nil,
hidden: [_touched: "date,instances", _form_type: "create"],
params: %{
"date" => "1111-01-11",
"instances" => %{
"0" => %{
"_form_type" => "read",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,dossier_number",
"dossier_number" => "2024-1"
}
}
},
errors: [],
options: [method: "post"],
index: nil
}
I can’t really explain what is happening here. Do I have to clean up these forms somehow? Is this a bug?






















