swoosh-emails
Skill4 starsChanged 4 months agoMITView on GitHub
Use when sending emails from Phoenix applications. Invoke before implementing email functionality. Covers Swoosh setup, email templates, delivery configuration, testing, and production adapters. Trigger words: email, Swoosh, mailer, email templates, SMTP, SendGrid, email testing.
- Reads credentials
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- apply-phoenix-controller-conventionsSkill
- ash-frameworkSkill
- benchee-profilingSkill
- broadway-data-pipelinesSkill
- cachex-cachingSkill
- code-qualitySkill
- code-reviewSkill
- deployment-gotchasSkill
- ecto-changeset-patternsSkill
- ecto-essentialsSkill
- ecto-migrationSkill
- ecto-nested-associationsSkill
- elixir-essentialsSkill
- gettext-i18nSkill
- liveview-streamsSkill
- mix-tasks-generatorsSkill
- oban-essentialsSkill
- otp-essentialsSkill
- phoenix-authenticationSkill
- phoenix-authorization-patternsSkill
- phoenix-channels-essentialsSkill
- phoenix-json-apiSkill
- phoenix-liveview-essentialsSkill
- phoenix-pubsub-patternsSkill
- phoenix-uploadsSkill
- property-based-testingSkill
- refactor-codeSkill
- req-http-clientSkill
- respond-to-reviewSkill
- security-essentialsSkill
- telemetry-essentialsSkill
- testing-essentialsSkill
- typespec-dialyzerSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 52
After adding deps and configuring the supervision tree, confirm everything works before writing email modules:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Swoosh Emails
RULES — Follow these with no exceptions
1. Define emails in separate modules — MyApp.Emails.UserEmail, not inline in contexts
2. Use Phoenix components for email templates — reuse UI components in emails
3. Configure delivery per environment — Local adapter in dev/test, real adapter in prod
4. Test emails with Swoosh.TestAssertions — assert emails were sent with correct content
5. Never send emails synchronously in web requests — use Oban for async delivery; Task.start only for simple cases
Setup
# mix.exs
defp deps do
[
{:swoosh, "~> 1.14"},
{:finch, "~> 0.18"},
{:gen_smtp, "~> 1.0"}
]
end
# application.ex
def start(_type, _args) do
children = [
# ...
{Finch, name: MyApp.Finch}
]
# ...
endValidate Setup
After adding deps and configuring the supervision tree, confirm everything works before writing email modules:
# In iex -S mix (dev environment with Local adapter)
MyApp.Mailer.deliver(Swoosh.Email.new(to: "test@example.com", from: "noreply@myapp.com", subject: "Test"))
# => {:ok, %{}} — mailer configured correctly
# => {:error, ...} — Finch missing from supervision tree or adapter misconfiguredDefining Emails
See assets/mailer_template.ex for a copy-paste template with the Mailer module, an email-builder module, and an Oban delivery worker.
With Phoenix Components
# lib/my_app/emails/user_email.ex
defmodule MyApp.Emails.UserEmail do
import Swoosh.Email
import Phoenix.Component, only: [sigil_H: 2]
alias MyAppWeb.EmailComponents
def welcome(user) do
assigns = %{user: user}
html =
~H"""
<EmailComponents.layout>
<h1>Welcome, <%= @user.name %>!</h1>
<p>Thanks for signing up.</p>
<EmailComponents.button href={url(~p"/dashboard")}>Get Started</EmailComponents.button>
</EmailComponents.layout>
"""
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
new()
|> to({user.name, user.email})
|> from({"MyApp", "noreply@myapp.com"})
|> subject("Welcome to MyApp!")
|> html_body(html)
|> text_body("Welcome, #{user.name}! Thanks for signing up.")
end
endEmail Layout and Button Components
# lib/my_app_web/components/email_components.ex
defmodule MyAppWeb.EmailComponents do
use Phoenix.Component
def layout(assigns) do
~H"""
<html>
<body style="font-family: sans-serif; max-width: 600px; margin: auto;">
<%= render_slot(@inner_block) %>
</body>
</html>
"""
end
def button(assigns) do
~H"""
<a href={@href} style="background: #4F46E5; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none;">
<%= render_slot(@inner_block) %>
</a>
"""
end
endMailer Module
# lib/my_app/mailer.ex
defmodule MyApp.Mailer do
use Swoosh.Mailer, otp_app: :my_app
endConfiguration
# config/dev.exs
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Local
config :swoosh, serve: true
# config/test.exs
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Test
# config/runtime.exs (production)
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: System.get_env("SENDGRID_API_KEY")Sending Emails
With Oban
# lib/my_app/workers/send_welcome_email.ex
defmodule MyApp.Workers.SendWelcomeEmail do
use Oban.Worker, queue: :mailers, max_attempts: 3
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
case Accounts.get_user(user_id) do
nil ->
{:cancel, "user #{user_id} not found"}
user ->
email = MyApp.Emails.UserEmail.welcome(user)
case MyApp.Mailer.deliver(email) do
{:ok, _} -> {:ok, :sent}
{:error, reason} -> {:error, reason}
end
end
end
end
# Enqueue from context
def register_user(attrs) do
with {:ok, user} <- create_user(attrs) do
%{user_id: user.id}
|> MyApp.Workers.SendWelcomeEmail.new()
|> Oban.insert()
{:ok, user}
end
endWith Task (Simple Cases Only)
def register_user(attrs) do
with {:ok, user} <- create_user(attrs) do
Task.start(fn ->
user |> UserEmail.welcome() |> Mailer.deliver()
end)
{:ok, user}
end
endTesting Emails
defmodule MyApp.AccountsTest do
use MyApp.DataCase, async: true
import Swoosh.TestAssertions
test "sends welcome email on registration" do
attrs = %{email: "test@example.com", password: "password123"}
assert {:ok, user} = Accounts.register_user(attrs)
assert_email_sent(fn email ->
assert email.to == [{user.name, user.email}]
assert email.subject =~ "Welcome"
end)
end
test "no email sent on failed registration" do
attrs = %{email: "", password: ""}
assert {:error, _changeset} = Accounts.register_user(attrs)
assert_no_email_sent()
end
endEmail Preview in Development
# config/dev.exs
config :swoosh, serve: true
# Access preview at http://localhost:4000/dev/mailboxCommon Pitfalls
| ❌ Don't | ✅ Do |
| Build emails inline inside a context | Define them in dedicated modules (MyApp.Emails.UserEmail) |
Call Mailer.deliver/1 inside a web request | Enqueue an Oban job (or Task.start for simple cases) so the request never blocks on SMTP |
| Ship an HTML-only email | Always set both html_body/2 and text_body/2 |
| Use a real adapter in dev/test | Swoosh.Adapters.Local in dev, Swoosh.Adapters.Test in test, real adapter only in prod |
| Hardcode the API key in config | Read it at runtime: System.get_env("SENDGRID_API_KEY") in runtime.exs |
| Assert delivery by inspecting logs | Use Swoosh.TestAssertions — assert_email_sent/1 and assert_no_email_sent/0 |
Forget {Finch, name: MyApp.Finch} in the supervision tree | Start Finch so API-based adapters have an HTTP client |
Integration
| Predecessor | This Skill | Successor |
| phoenix-auth-customization | swoosh-emails | oban-essentials |
| ecto-essentials | swoosh-emails | testing-essentials |
Companion skills:
oban-essentials— deliver emails asynchronously with retriestesting-essentials— assert delivery withSwoosh.TestAssertions
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

