Default create action doesn't work with allow_nil? belongs_to relationships

If you define a belongs_to relationship with allow_nil? true, the default create action becomes unusable; any attempt to use it will fail because the relationship’s attribute is not set.

Consider the following resource with a belongs_to relationship:

defmodule Helpdesk.Support.Ticket do
  use Ash.Resource

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  attributes do
    uuid_primary_key :id

    attribute :subject, :string
  end

  relationships do
    belongs_to :representative, Helpdesk.Support.Representative do
      allow_nil? false
    end
  end
end

One would assume that this would allow the user to create a Ticket by passing a Representative in the input to the create action, like:

Helpdesk.Support.Ticket |> Ash.Changeset.for_create(:create, %{representative: rep}) |> Helpdesk.Support.create!()

But this raises an error that “relationship representative is required”, despite it being provided.

To add to the confusion, this is inconsistent with the behaviour in other situations. For example Ash.Seed.seed!(%Helpdesk.Support.Ticket{representative: rep}) will create the Ticket record as expected, with the relationship attribute inferred correctly. Or if Representative had an update action to create an associated Ticket:

defmodule Helpdesk.Support.Representative do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets

  actions do
    defaults [:create, :read, :update, :destroy]

    update :add_ticket do
      argument :subject, :string
      change manage_relationship(:subject, :tickets, value_is_key: :subject, type: :create)
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :name, :string
  end

  relationships do
    has_many :tickets, Helpdesk.Support.Ticket
  end
end

Calling the add_ticket action on a Representative will successfully create a new related Ticket:

rep |> Ash.Changeset.for_update(:add_ticket, %{subject: "blah blah"}) |> Helpdesk.Support.update!()
#Helpdesk.Support.Representative<
  tickets: [
    #Helpdesk.Support.Ticket<
      representative: #Ash.NotLoaded<:relationship, field: :representative>,
      __meta__: #Ecto.Schema.Metadata<:built, "">,
      id: "f403066a-cf0e-43f8-a3f4-b40fac8a7d27",
      subject: "blah blah",
      representative_id: "bd0e5d0b-5903-4083-ab9f-3fb42790e220",
      aggregates: %{},
      calculations: %{},
      ...
    >
  ],
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "bd0e5d0b-5903-4083-ab9f-3fb42790e220",
  name: "test",
  aggregates: %{},
  calculations: %{},
  ...
>

This is doubly confusing because the add_ticket update action uses Ticket’s default create action to create the new related Ticket, but in a way that user’s seemingly are not.

I know there are ways to get this to work, either by making the relationship attribute_writable? true and passing the associated ID directly or by defining a custom create action that uses manage_relationship, but neither of those are intuitive solutions. I think the default create action should be able to handle passing the related record as part of the input without needing any extra code.

This is using ash version 2.19.9.

This is caused by the default of attribute_writable? being false. IIRC this will change to true in 3.0 and make this more intuitive to the user.

Although, this would only allow you to pass in the represenative_id, not the whole representative record by default.