agentleFS
igmarinigmarin/elixir-phoenix-skills/skills/swoosh-emails/SKILL.md

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

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
  1. Distinctive:

    After adding deps and configuring the supervision tree, confirm everything works before writing email modules:

    line 52

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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}
  ]
  # ...
end

Validate 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 misconfigured

Defining 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
end

Email 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
end

Mailer Module

# lib/my_app/mailer.ex
defmodule MyApp.Mailer do
  use Swoosh.Mailer, otp_app: :my_app
end

Configuration

# 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
end

With 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
end

Testing 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
end

Email Preview in Development

# config/dev.exs
config :swoosh, serve: true

# Access preview at http://localhost:4000/dev/mailbox

Common Pitfalls

❌ Don't✅ Do
Build emails inline inside a contextDefine them in dedicated modules (MyApp.Emails.UserEmail)
Call Mailer.deliver/1 inside a web requestEnqueue an Oban job (or Task.start for simple cases) so the request never blocks on SMTP
Ship an HTML-only emailAlways set both html_body/2 and text_body/2
Use a real adapter in dev/testSwoosh.Adapters.Local in dev, Swoosh.Adapters.Test in test, real adapter only in prod
Hardcode the API key in configRead it at runtime: System.get_env("SENDGRID_API_KEY") in runtime.exs
Assert delivery by inspecting logsUse Swoosh.TestAssertions — assert_email_sent/1 and assert_no_email_sent/0
Forget {Finch, name: MyApp.Finch} in the supervision treeStart Finch so API-based adapters have an HTTP client

Integration

PredecessorThis SkillSuccessor
phoenix-auth-customizationswoosh-emailsoban-essentials
ecto-essentialsswoosh-emailstesting-essentials

Companion skills:

  • oban-essentials — deliver emails asynchronously with retries
  • testing-essentials — assert delivery with Swoosh.TestAssertions

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.