Recipient Field Extraction
View SourceAshDispatch uses a transport-first configuration approach for extracting recipient information. Each transport (email, SMS, Discord, in-app) can have different identifier and display name fields, with optional per-audience overrides.
API Documentation: For complete function documentation, see
AshDispatch.Event.RecipientExtractor.
Why Transport-First?
Different transports need different recipient data:
- Email needs an email address
- SMS needs a phone number
- Discord needs a Discord ID
- In-app needs a user ID
Rather than forcing a generic "identifier" that doesn't fit all cases, you configure each transport's needs explicitly.
Quick Start
Minimal Configuration
Configure the transports you use:
# config/config.exs
config :ash_dispatch,
recipient_fields: [
email: [
identifier: :email,
name: :contact_person
],
in_app: [
identifier: :id,
name: :contact_person
]
]That's it! AshDispatch now knows how to extract recipient info for email and in-app notifications.
Adding More Transports
Add other transports as you need them:
config :ash_dispatch,
recipient_fields: [
email: [
identifier: :email,
name: [:display_name, :name, :contact_person] # fallback chain
],
sms: [
identifier: :mobile_phone,
name: [:first_name, :name]
],
discord: [
identifier: :discord_id,
name: [:app_username, :display_name, :name]
],
in_app: [
identifier: :id,
name: [:display_name, :name]
]
]Fallback Chains
For the name field, you can specify a list of fields to try in order:
name: [:display_name, :name, :contact_person, :full_name]The extractor tries each field until one returns a value. This is useful when your User model might have different name fields populated.
Per-Audience Overrides
Different audience types might have different field names. Configure overrides per audience:
config :ash_dispatch,
recipient_fields: [
# Default transport configuration
email: [
identifier: :email,
name: [:display_name, :name]
],
# Audience-specific overrides
audiences: [
# Admins show full name in emails
admin: [
email: [name: :full_name]
],
# Customers use company info
customer: [
email: [
identifier: :contact_email,
name: [:company_name, :contact_person]
]
]
]
]When sending to :admin audience via :email transport:
- Uses
:emailfor identifier (from transport default) - Uses
:full_namefor name (from audience override)
Resolution Order
The system resolves fields in this order (most specific first):
- Event DSL override - Configured directly in the event definition
- Audience + Transport -
audiences: [admin: [email: [name: :full_name]]] - Transport default -
email: [identifier: :email] - Error - Clear error message with guidance
Event-Level Overrides
Override fields for specific events using the DSL:
# In your resource
dispatch do
event :special_case,
trigger_on: :special_action,
recipient: [
email: [
identifier: :backup_email,
name: :preferred_name
]
]
endOr in standalone event modules:
defmodule MyApp.Events.SpecialEvent do
use AshDispatch.Event
dispatch do
id "special.event"
recipient do
email do
identifier :alternate_email
name :display_name
end
end
channels do
channel :email, :user
end
end
endAdvanced Field Formats
Simple Atom (Most Common)
identifier: :email
name: :contact_personFallback Chain (List of Atoms)
name: [:display_name, :name, :contact_person, :full_name]Tries each field in order until one returns a value.
Nested Fields
For data stored in nested structures:
identifier: {:field, [:contact, :email]}
name: {:field, [:profile, :display_name]}Works with structs like:
%User{
contact: %{email: "user@example.com"},
profile: %{display_name: "John"}
}String Keys (JSON Data)
For maps with string keys:
identifier: {:string_field, "email_address"}Custom Functions
For complex extraction logic:
identifier: &MyApp.extract_primary_contact/1
name: &MyApp.format_recipient_name/1defmodule MyApp do
def extract_primary_contact(recipient) do
recipient.emails
|> Enum.find(&(&1.primary == true))
|> Map.get(:address)
end
def format_recipient_name(recipient) do
"#{recipient.first_name} #{recipient.last_name}"
end
endCiString Support
AshDispatch automatically unwraps Ash.CiString (case-insensitive string) values. If your email field uses Ash.Type.CiString, it will be extracted correctly without any additional configuration.
Clear Error Messages
If configuration is missing, you get helpful guidance:
No identifier field configured for email transport (user audience).
AshDispatch doesn't assume field names - you must configure them.
Add transport config to config/config.exs:
config :ash_dispatch,
recipient_fields: [
email: [
identifier: :your_field_name
]
]
Or add audience-specific override:
config :ash_dispatch,
recipient_fields: [
email: [identifier: :default_field],
audiences: [
user: [
email: [identifier: :your_field]
]
]
]If a field is missing from the recipient:
Could not extract identifier from recipient for email transport (user audience).
Expected field: :email
Recipient type: MyApp.Accounts.Lead
Available keys: [:id, :primary_email, :company_name]
Fix options:
1. Add field :email to your recipient struct
2. Change config to use an existing field (:primary_email)
3. Use custom extraction functionReal-World Examples
E-commerce Platform
config :ash_dispatch,
recipient_fields: [
email: [
identifier: :email,
name: [:full_name, :name]
],
sms: [
identifier: :phone_number,
name: :first_name
],
in_app: [
identifier: :id,
name: [:full_name, :name]
]
]B2B Platform
config :ash_dispatch,
recipient_fields: [
email: [
identifier: :email,
name: [:contact_person, :company_name]
],
in_app: [
identifier: :id,
name: [:contact_person, :company_name]
],
audiences: [
# Leads use different fields
lead: [
email: [
identifier: :primary_email,
name: :company_name
]
],
# Partners have nested contact info
partner: [
email: [
identifier: {:field, [:primary_contact, :email]},
name: {:field, [:primary_contact, :name]}
]
]
]
]Multi-Channel Platform with Discord
config :ash_dispatch,
recipient_fields: [
email: [
identifier: :email,
name: [:display_name, :name]
],
discord: [
identifier: :discord_id,
name: [:discord_username, :display_name, :name]
],
slack: [
identifier: :slack_id,
name: [:slack_display_name, :name]
],
in_app: [
identifier: :id,
name: [:display_name, :name]
]
]Testing
The RecipientExtractor is fully testable:
test "extracts identifier from custom struct" do
recipient = %{
id: "123",
primary_email: "test@example.com",
full_name: "Test User"
}
# With custom config
event_config = %{
recipient: %{
email: %{
identifier: :primary_email,
name: :full_name
}
}
}
identifier = RecipientExtractor.extract_identifier(
recipient,
:email,
:user,
event_config[:recipient]
)
assert identifier == "test@example.com"
endSee Also
- Configuration Guide - Complete configuration reference
- Recipient Resolution - How audiences resolve to recipients
AshDispatch.Event.RecipientExtractor- Module documentation