# Recipient Field Extraction
AshDispatch 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:
```elixir
# 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:
```elixir
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:
```elixir
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:
```elixir
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:
1. Uses `:email` for identifier (from transport default)
2. Uses `:full_name` for name (from audience override)
## Resolution Order
The system resolves fields in this order (most specific first):
1. **Event DSL override** - Configured directly in the event definition
2. **Audience + Transport** - `audiences: [admin: [email: [name: :full_name]]]`
3. **Transport default** - `email: [identifier: :email]`
4. **Error** - Clear error message with guidance
## Event-Level Overrides
Override fields for specific events using the DSL:
```elixir
# In your resource
dispatch do
event :special_case,
trigger_on: :special_action,
recipient: [
email: [
identifier: :backup_email,
name: :preferred_name
]
]
end
```
Or in standalone event modules:
```elixir
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
end
```
## Advanced Field Formats
### Simple Atom (Most Common)
```elixir
identifier: :email
name: :contact_person
```
### Fallback Chain (List of Atoms)
```elixir
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:
```elixir
identifier: {:field, [:contact, :email]}
name: {:field, [:profile, :display_name]}
```
Works with structs like:
```elixir
%User{
contact: %{email: "user@example.com"},
profile: %{display_name: "John"}
}
```
### String Keys (JSON Data)
For maps with string keys:
```elixir
identifier: {:string_field, "email_address"}
```
### Custom Functions
For complex extraction logic:
```elixir
identifier: &MyApp.extract_primary_contact/1
name: &MyApp.format_recipient_name/1
```
```elixir
defmodule 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
end
```
## CiString 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 function
```
## Real-World Examples
### E-commerce Platform
```elixir
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
```elixir
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
```elixir
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:
```elixir
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"
end
```
## See Also
- [Configuration Guide](configuration.html) - Complete configuration reference
- [Recipient Resolution](recipient-resolution.html) - How audiences resolve to recipients
- `AshDispatch.Event.RecipientExtractor` - Module documentation