---
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).