|
| 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> |
0 commit comments