--- title: "Email" author: "@chris" author_url: https://elements.dev/u/chris published: 2026-10-02T15:11:56.365Z url: https://elements.dev/feed/01a0fd2c-04d3-711d-a17d-f194eabdc237 kind: lesson format: article --- # Email by [@chris](https://elements.dev/u/chris) ยท 2026-10-02 ## Description Write emails in the same HTML as your pages and send them with one call. Email templates in Elements are written in the same HTML as your pages, and sending one is a single function call. The build inlines the template's styles and makes its links absolute, so the email renders in mail clients the way it renders in your browser. ## Create an email template ```bash Terminal elements create email welcome ``` ```ehtml app/emails/welcome/index.ehtml import "./style.css"; /** @email */

Welcome, {name}

Thanks for signing up.

``` The `@email` build tag on `` marks the template as an email. It has typed attributes like any template, and nothing in it is sent to a browser. ## Send an email ```typescript app/jobs/send-welcome.ts import { email } from "@elements/app"; import WelcomeEmail from "#app/emails/welcome"; email({ to: user.email, subject: "Welcome", body: new WelcomeEmail({ name: user.name }), }); ``` `email()` works in any server code: a route, an RPC function, a job or a helper they call. It sends right away. ## Send email from a job `email()` talks to the mail server while your code waits. In a page's RPC function that wait holds up the request, and if the mail server is down, the email is lost. Sending from a job fixes both: the job runs in a separate worker process, and it retries until the mail server answers. ```typescript app/jobs/send-welcome.ts import { Job, email } from "@elements/app"; import WelcomeEmail from "#app/emails/welcome"; export interface SendWelcomeJobFields { to: string; name: string; } export class SendWelcomeJob extends Job { static maxAttempts = 5; run() { let { to, name } = this.fields; email({ to, subject: "Welcome", body: new WelcomeEmail({ name }), }); } } ``` ## Email and transactions `email()` sends right away and is not part of a transaction. Inside `tx()`, a statement that throws before `email()` stops the code there, so the email is never sent, the same as any line after a throw. The case to watch is a statement that throws after `email()` has run: the transaction rolls back, but the email has already gone out. Scheduling the job inside the transaction instead ties the email to the commit. The job is a row in the same transaction, so if anything after it throws, the job is rolled back with everything else and the email is never sent: ```typescript app/pages/signup/services.ts import { tx } from "@elements/app"; import { SendWelcomeJob } from "#app/jobs/send-welcome"; /** @rpc */ export function signup(form: SignupForm): User { return tx(() => { let user = createUser(form); new SendWelcomeJob({ to: user.email, name: user.name, }).schedule(); createDefaultWorkspace(user); return user; }); } ``` If `createDefaultWorkspace` throws, the user, the workspace and the welcome email are all rolled back together. ## In development By default, nothing is sent while you build. Each email is written to the project server log instead, with its recipients, subject and body, so you need no mail account to get started. To look at an email as it will render, return the template from a route and open that route in your browser: ```typescript app/pages/email-preview/index.ts import WelcomeEmail from "#app/emails/welcome"; export default function route() { return new WelcomeEmail({ name: "Ada" }); } ``` Register that route in development only. Then edit the template and reload the page to see each change. ## Sending live emails To deliver real email, your app needs an email sending service. Your app hands each email to the service, and the service delivers it to the recipient's inbox. Postmark, Amazon SES, Resend, SendGrid and Mailgun are common choices, and most offer a free tier to start with. Apps hand email to these services over SMTP, the standard protocol for sending mail, and Elements works with any service that supports it, which they all do. When you sign up, the service gives you four SMTP settings: a host, a port, a username and a password. Most also ask you to verify the domain you send from, by adding a few DNS records they show you, so your email does not land in spam. Put those settings in the environment file for each environment that should send real email, and set `EMAIL_LIVE=true` there: ```text config/env/production.env EMAIL_LIVE=true SMTP_HOST=smtp.postmarkapp.com SMTP_PORT=587 SMTP_USER=your-username SMTP_PASSWORD=your-password ``` This works in any environment, development included, so you can send real email to yourself while you build. `config/env/production.env` is never committed, which keeps the password out of your repository. `development.env` is committed, so to send live email in development, set the password in your shell instead of in that file. In production, an app that is not set to send live email refuses to start. An email that should go out never ends up silently written to a log. ## See email in a demo - [Millrow](https://elements.dev/demos/01a0f39d-0703-7846-ac3c-a5b583a5727c), a coffee store that emails each order. - [Letterhearth](https://elements.dev/demos/01a0f455-f25d-784c-a9d9-35a5841308ce), a paid newsletter with scheduled sends. The manual: [email](/learn/man/email), [jobs](/learn/man/jobs), [config](/learn/man/config).