Generated Email
A draft cold outbound email grounded in real, recent facts about the recipient. Every claim about the recipient is backed by a source.
Demo
Enter a recipient (person, company, or both). Exa researches them and drafts an email from the sender configured in email-sender.ts.
Try one of these:
How it works
One call to exa.search with type: "deep" and an outputSchema that returns a structured object with subject and body fields.
The systemPrompt carries the sender persona (name, product, ICP) and a tight list of style rules (no clichés, no fake intimacy, no em dashes). The schema description shapes the body into 3-4 short paragraphs anchored by a recent, specific fact about the recipient.
The body stays clean: no inline citation markers. Sources are flattened from output.grounding and rendered as a favicon row below the email, deduped by domain.
Source code
"use client";
import { useState } from "react";
import { CitationChip, type Citation } from "@/lib/citations";
import { SENDER } from "@/lib/email-sender";
type EmailState =
| { status: "idle" }
| { status: "generating" }
| {
status: "done";
recipient: string;
subject: string;
body: string;
citations: Citation[];
}
| { status: "error"; error: string };
const SUGGESTIONS = [
"Patrick Collison, CEO at Stripe",
"Cursor (the AI code editor)",
"Mercury, the fintech bank",
];
export function EmailExample() {
const [recipient, setRecipient] = useState("");
const [email, setEmail] = useState<EmailState>({ status: "idle" });
async function generate(input: string) {
const r = input.trim();
if (!r || email.status === "generating") return;
setEmail({ status: "generating" });
try {
const res = await fetch("/api/email", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ recipient: r }),
});
const json = await res.json();
if (!res.ok || json.error) {
setEmail({
status: "error",
error: json.error || `Request failed (${res.status})`,
});
return;
}
setEmail({
status: "done",
recipient: json.recipient ?? r,
subject: json.subject ?? "",
body: json.body ?? "",
citations: json.citations ?? [],
});
} catch (err) {
setEmail({
status: "error",
error: err instanceof Error ? err.message : String(err),
});
}
}
const isGenerating = email.status === "generating";
return (
<div className="space-y-4">
<form
onSubmit={(e) => {
e.preventDefault();
void generate(recipient);
}}
className="border border-[#e5e5e5] bg-white overflow-hidden focus-within:border-[var(--color-brand)]"
>
<div className="flex items-center gap-2 px-3 py-2">
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="Recipient: person, company, or both"
className="flex-1 text-[13px] text-[#111827] focus:outline-none"
/>
<button
type="submit"
disabled={isGenerating || !recipient.trim()}
className="bg-[var(--color-brand)] text-white text-[13px] font-medium px-3.5 py-1.5 disabled:opacity-50"
>
{isGenerating ? "Drafting..." : "Draft"}
</button>
</div>
</form>
{email.status === "idle" && (
<div className="text-[13px] text-[#6b7280]">
<p className="mb-2">Try one of these:</p>
<div className="flex flex-wrap gap-2">
{SUGGESTIONS.map((s) => (
<button
key={s}
type="button"
onClick={() => {
setRecipient(s);
void generate(s);
}}
className="rounded-full border border-[#e5e5e5] bg-white px-3 py-1 text-[12px] text-[#374151] hover:border-[var(--color-brand)] hover:text-[#111827]"
>
{s}
</button>
))}
</div>
</div>
)}
{email.status === "error" && (
<div className="text-[13px] text-[#b91c1c]">{email.error}</div>
)}
{email.status === "generating" && (
<div className="border border-[#e5e5e5] bg-white px-6 py-5">
<DraftSkeleton />
</div>
)}
{email.status === "done" && (
<EmailCard
recipient={email.recipient}
subject={email.subject}
body={email.body}
citations={email.citations}
/>
)}
</div>
);
}
function EmailCard({
recipient,
subject,
body,
citations,
}: {
recipient: string;
subject: string;
body: string;
citations: Citation[];
}) {
const paragraphs = body.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
// Dedupe citations by domain so the sources row shows distinct
// sources only.
const seen = new Set<string>();
const uniqueCitations: Citation[] = [];
for (const c of citations) {
const d = safeDomain(c.url);
if (seen.has(d)) continue;
seen.add(d);
uniqueCitations.push(c);
}
return (
<div className="border border-[#e5e5e5] bg-white">
<div className="px-6 py-3 border-b border-[#e5e5e5] space-y-1">
<HeaderRow label="From" value={`${SENDER.name} · ${SENDER.product}`} />
<HeaderRow label="To" value={recipient} />
<HeaderRow label="Subject" value={subject} bold />
</div>
<div className="px-6 py-5 space-y-3 text-[13px] text-[#111827] leading-[1.7]">
{paragraphs.map((p, i) => (
<p key={i} className="whitespace-pre-line">
{p}
</p>
))}
</div>
{uniqueCitations.length > 0 && (
<div className="px-6 py-3 border-t border-[#e5e5e5] flex items-center gap-2 flex-wrap">
<span className="inline-flex items-center h-4 text-[11px] uppercase tracking-wider text-[#9ca3af] leading-none mr-1">
Sources
</span>
<span className="inline-flex items-center gap-1">
{uniqueCitations.map((c) => (
<CitationChip key={c.id} citation={c} />
))}
</span>
</div>
)}
</div>
);
}
function safeDomain(url: string): string {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return url;
}
}
function HeaderRow({
label,
value,
bold,
}: {
label: string;
value: string;
bold?: boolean;
}) {
return (
<div className="flex gap-2 text-[12px] leading-[1.55]">
<span className="text-[#9ca3af] w-14 shrink-0">{label}</span>
<span
className={`text-[#111827] ${bold ? "font-medium" : ""} min-w-0 break-words`}
>
{value}
</span>
</div>
);
}
function DraftSkeleton() {
return (
<div className="space-y-2">
<div className="h-3 w-1/3 rounded bg-[#e5e7eb] animate-pulse" />
<div className="h-3 w-1/2 rounded bg-[#e5e7eb] animate-pulse" />
<div className="h-4 w-2/3 rounded bg-[#e5e7eb] animate-pulse" />
<div className="h-2" />
<div className="h-3 w-full rounded bg-[#e5e7eb] animate-pulse" />
<div className="h-3 w-11/12 rounded bg-[#e5e7eb] animate-pulse" />
<div className="h-3 w-3/4 rounded bg-[#e5e7eb] animate-pulse" />
</div>
);
}