Skip to content

Commit 1fbf96e

Browse files
jackbridgerJack Bridger
andauthored
docs: Resend email agent example guide (#5081)
Adds an example project guide for the new [Resend email agent example](https://github.com/triggerdotdev/examples/tree/main/resend-slack-email-agent) (triggerdotdev/examples#157): an AI agent that drafts replies to a Resend inbox and waits for approval in Slack before sending. - New page `guides/example-projects/resend-email-agent`, registered in `docs.json` and the example projects table. It covers the demo video, how the webhooks and wait tokens fit together, setup steps, and links into the repo. - One sentence on `webhooks/sources`, under the table where `resend()` is listed, linking to the example. Demo video, embedded on the page from YouTube: https://www.youtube.com/watch?v=6GjXLwj_dnE The repo links go live when triggerdotdev/examples#157 merges, so merge that first. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Jack Bridger <jack@trigger.dev>
1 parent 4ed2e37 commit 1fbf96e

4 files changed

Lines changed: 170 additions & 0 deletions

File tree

‎docs/docs.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@
618618
"guides/example-projects/product-image-generator",
619619
"guides/example-projects/realtime-csv-importer",
620620
"guides/example-projects/realtime-fal-ai",
621+
"guides/example-projects/resend-email-agent",
621622
"guides/example-projects/smart-spreadsheet",
622623
"guides/example-projects/turborepo-monorepo-prisma",
623624
"guides/example-projects/vercel-ai-sdk-deep-research",
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
title: "Resend email agent with Slack approval"
3+
sidebarTitle: "Resend email agent"
4+
description: "An AI agent that drafts replies to a Resend inbox and sends nothing until you approve it in Slack. Uses hosted webhooks for Resend and Slack, and wait tokens to pause each email thread until you click Send."
5+
---
6+
7+
<iframe
8+
width="100%"
9+
height="315"
10+
src="https://www.youtube.com/embed/6GjXLwj_dnE"
11+
title="Resend email agent with Slack approval"
12+
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
13+
referrerPolicy="strict-origin-when-cross-origin"
14+
allowFullScreen
15+
/>
16+
17+
## Overview
18+
19+
This agent drafts replies to the email in a [Resend Inboxes](https://resend.com/docs/api-reference/inboxes) inbox and posts each draft to your Slack DMs for approval. Every email chain gets its own Slack thread. Click **Send** to reply from the inbox address, **Don't send** to drop the draft, or reply in the thread ("shorter", "say we're closed over Christmas") to get a new draft. Later emails in the same chain land in the same Slack thread.
20+
21+
**Tech stack:**
22+
23+
- **[Trigger.dev webhooks](/webhooks/overview)** receive Resend and Slack events and verify their signatures
24+
- **[Wait tokens](/wait-for-token)** pause each email thread's run until you act in Slack
25+
- **[Resend Inboxes](https://resend.com/docs/api-reference/inboxes)** receive email and send the approved replies
26+
- **[AI SDK](https://ai-sdk.dev)** drafts replies through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway)
27+
- **[Slack](https://api.slack.com)** for the DM-only approval app
28+
29+
**Features:**
30+
31+
- **A person sends every reply**: the model has no tools, so a hostile email can only change the words of a draft you read first.
32+
- **Feedback in the thread**: replying to a draft writes a new one that takes your note into account.
33+
- **One run per email chain**: a run waits days for your click without holding a machine or a concurrency slot.
34+
- **Sender allowlist**: `ALLOWED_SENDER_DOMAINS` drops unknown senders before they reach Slack or cost a model call.
35+
- **Idempotent sends**: each inbound email gets at most one reply, even when a run retries.
36+
37+
## GitHub repo
38+
39+
<Card
40+
title="View the Resend email agent repo"
41+
icon="GitHub"
42+
href="https://github.com/triggerdotdev/examples/tree/main/resend-slack-email-agent"
43+
>
44+
Click here to view the full code for this project in our examples repository on GitHub. You can
45+
fork it and use it as a starting point for your own project.
46+
</Card>
47+
48+
## How it works
49+
50+
Each email chain gets one long-running `email-thread` run that sleeps on a wait token between events. The token is tagged with the email thread id and the Slack message timestamp, so a webhook from either side can find it and wake the run.
51+
52+
```mermaid
53+
flowchart LR
54+
E([New email]) --> R[resend-email-received<br/>webhook task]
55+
C([Button click]) --> S[slack-activity<br/>webhook task]
56+
T([Thread reply]) --> S
57+
R -- wakes or starts --> RUN[email-thread run<br/>waits on a token]
58+
S -- completes token --> RUN
59+
RUN -- email, draft, buttons --> DM[Your Slack DM]
60+
RUN -- after you click Send --> SEND[Resend reply]
61+
```
62+
63+
### Hosted webhook endpoints
64+
65+
Two endpoints receive events, one for Resend and one for Slack. Trigger.dev checks each request's signature before your code runs, and the Slack source answers Slack's `url_verification` handshake.
66+
67+
```ts src/trigger/endpoints.ts
68+
import { webhooks } from "@trigger.dev/sdk";
69+
import { webhookSource, type SlackBlockActions, type SlackMessageEvent } from "@trigger.dev/slack";
70+
import type { ResendWebhookEvent } from "./resend";
71+
72+
export const resendEvents = webhooks.endpoint.define({
73+
id: "resend",
74+
source: webhooks.resend<ResendWebhookEvent>(),
75+
});
76+
77+
export const slackEvents = webhooks.endpoint.define({
78+
id: "slack",
79+
source: webhookSource<SlackMessageEvent | SlackBlockActions>(),
80+
});
81+
```
82+
83+
### Waiting for approval
84+
85+
After posting a draft, the run creates a wait token and waits on it. The token is created before the draft is posted, so a fast click always has something to complete. Each button carries the token id.
86+
87+
```ts src/trigger/email-thread.ts
88+
const token = await wait.createToken({
89+
timeout: "30d",
90+
tags: [emailTag(threadId), slackTag(slackTs)],
91+
});
92+
draftTs = await post(channel, "Draft reply", draftBlocks(draft, token.id), slackTs);
93+
94+
const result = await wait.forToken<Signal>(token);
95+
```
96+
97+
### Waking the run
98+
99+
The webhook tasks find the waiting token by tag and complete it with what happened: a button click, a reply in the thread, or a new email. The run then sends the reply, drops it, or redrafts with your note, and waits again.
100+
101+
```ts src/trigger/inbound.ts
102+
const token = await waitingToken(slackTag(msg.thread_ts));
103+
if (token) await wait.completeToken<Signal>(token, { kind: "feedback", user: msg.user, text });
104+
```
105+
106+
## Running the project
107+
108+
You need a Trigger.dev project, a Resend inbox on a verified domain, a Slack workspace where you can create apps, and a Vercel AI Gateway key.
109+
110+
<Steps>
111+
<Step title="Clone the example and install">
112+
```bash
113+
git clone https://github.com/triggerdotdev/examples.git
114+
cd examples/resend-slack-email-agent
115+
cp .env.example .env
116+
pnpm install
117+
```
118+
119+
Set `TRIGGER_PROJECT_REF` in `.env` to your project's ref, from the project settings page in the dashboard. Then run `pnpm dev`, which adds two endpoints, `resend` and `slack`, to your dev environment's **Webhooks** page.
120+
</Step>
121+
<Step title="Connect Resend">
122+
Add a Resend webhook pointing at the `resend` endpoint URL with the `inbox.email.received` event. Paste its signing secret into the `resend` endpoint, and set `RESEND_API_KEY`.
123+
</Step>
124+
<Step title="Connect Slack">
125+
Put the `slack` endpoint URL in `slack/manifest.yaml` and create the app from it. Paste its Signing Secret into the `slack` endpoint, click **Verify** in Event Subscriptions, install the app, and set `SLACK_BOT_TOKEN`.
126+
</Step>
127+
<Step title="Set who approves and who can email">
128+
Set `SLACK_USER_ID` to your Slack member ID, `ALLOWED_SENDER_DOMAINS` to the domains you accept email from, and `AI_GATEWAY_API_KEY`. Restart `pnpm dev`.
129+
</Step>
130+
<Step title="Send a test email">
131+
Email your inbox from an allowed address. A DM from the bot shows the email with a draft in its thread, and the dev terminal shows a `resend-email-received` run followed by an `email-thread` run that stays waiting.
132+
</Step>
133+
</Steps>
134+
135+
To deploy, run `npx trigger.dev@latest deploy`. Production has its own `resend` and `slack` endpoints, with their own URLs and secrets:
136+
137+
1. **Slack:** create a second Slack app from the manifest with the production `slack` URL. Paste its Signing Secret into the production `slack` endpoint, then click **Verify** in the app's Event Subscriptions. Install the app and use its bot token as `SLACK_BOT_TOKEN` in production. A Slack app sends its clicks and replies to one URL, so dev and production each need their own app.
138+
2. **Resend:** point a Resend webhook at the production `resend` URL and paste its signing secret into the production `resend` endpoint. Disable or delete the dev webhook, or use a separate inbox for dev, so the same email doesn't get drafted twice.
139+
3. Set the other environment variables on your production environment as in dev.
140+
141+
An endpoint without its secret shows "Needs secret" on the Webhooks page.
142+
143+
## Relevant code
144+
145+
- **Email thread task**: [src/trigger/email-thread.ts](https://github.com/triggerdotdev/examples/blob/main/resend-slack-email-agent/src/trigger/email-thread.ts): one run per chain that posts, drafts, waits on a token and acts on the result
146+
- **Webhook tasks**: [src/trigger/inbound.ts](https://github.com/triggerdotdev/examples/blob/main/resend-slack-email-agent/src/trigger/inbound.ts): wake or start a thread's run from Resend emails, Slack clicks and thread replies
147+
- **Drafting prompt**: [src/trigger/draft.ts](https://github.com/triggerdotdev/examples/blob/main/resend-slack-email-agent/src/trigger/draft.ts): the AI SDK call and the system prompt to tune for your inbox
148+
- **Webhook endpoints**: [src/trigger/endpoints.ts](https://github.com/triggerdotdev/examples/blob/main/resend-slack-email-agent/src/trigger/endpoints.ts): the hosted Resend and Slack endpoints
149+
- **Slack app**: [slack/manifest.yaml](https://github.com/triggerdotdev/examples/blob/main/resend-slack-email-agent/slack/manifest.yaml): a DM-only app with three scopes
150+
151+
## Learn more
152+
153+
<CardGroup cols={2}>
154+
<Card title="Webhooks" icon="webhook" href="/webhooks/overview">
155+
Hosted endpoints that verify and deliver events to your tasks.
156+
</Card>
157+
<Card title="Wait for token" icon="hourglass" href="/wait-for-token">
158+
Pause a run until an external event completes a token.
159+
</Card>
160+
<Card title="Webhook sources" icon="plug" href="/webhooks/sources">
161+
Every built-in source, including `resend()`, and how each is verified.
162+
</Card>
163+
<Card title="Slack" icon="slack" href="/webhooks/slack">
164+
The Slack source and channel in `@trigger.dev/slack`.
165+
</Card>
166+
</CardGroup>

‎docs/guides/introduction.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Example projects are full projects with example repos you can fork and use. Thes
7070
| [Python web crawler](/guides/python/python-crawl4ai) | Use Python, Crawl4AI and Playwright to create a headless web crawler with Trigger.dev. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/python-crawl4ai) |
7171
| [Realtime CSV Importer](/guides/example-projects/realtime-csv-importer) | Upload a CSV file and see the progress of the task streamed to the frontend. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-csv-importer) |
7272
| [Realtime Fal.ai image generation](/guides/example-projects/realtime-fal-ai) | Generate an image from a prompt using Fal.ai and show the progress of the task on the frontend using Realtime. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-fal-ai-image-generation) |
73+
| [Resend email agent](/guides/example-projects/resend-email-agent) | An AI agent that drafts replies to a Resend inbox and waits for your approval in Slack before sending, using hosted webhooks and wait tokens. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/resend-slack-email-agent) |
7374
| [Smart Spreadsheet](/guides/example-projects/smart-spreadsheet) | Enrich company data using Exa search and Claude with real-time streaming results. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/smart-spreadsheet) |
7475
| [Turborepo monorepo with Prisma](/guides/example-projects/turborepo-monorepo-prisma) | Use Prisma in a Turborepo monorepo with Trigger.dev. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package) |
7576
| [Vercel AI SDK image generator](/guides/example-projects/vercel-ai-sdk-image-generator) | Use the Vercel AI SDK to generate images from a prompt. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-image-generator) |

‎docs/webhooks/sources.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ Supabase database webhooks aren't signed, so `supabase()` checks a shared secret
106106

107107
Slack also ships as a chat channel in `@trigger.dev/slack`.
108108

109+
For a complete project built on `resend()`, see the [Resend email agent example](https://github.com/triggerdotdev/examples/tree/main/resend-slack-email-agent). It drafts replies to a Resend inbox and waits for your approval in Slack before sending them.
110+
109111
### Hookdeck
110112

111113
`hookdeck()` verifies requests that a [Hookdeck](https://hookdeck.com) connection forwards to the endpoint. Hookdeck forwards the original provider's body unchanged, so pass that provider's event type:

0 commit comments

Comments
 (0)