K

Explore

Agent Templates

Build

/Support Agent

Support Agent: Answer From Docs, Escalate via Email

Deploy a support agent that answers questions from your documentation and automatically forwards anything it can't answer to your team via Resend - powered by 21st SDK.

Serafim Korablev
Serafim Korablev21st Founder
@serafimcloud

The Problem

Your docs answer 80% of support questions. The other 20% need a human. The problem is routing - someone asks a question, gets silence or a generic "check the docs" response, and churns before your team even knows they needed help.

What if your support agent could answer from the docs and automatically escalate anything it can't handle - right to your team's inbox?

How It Works

The Support Agent is built on top of the Docs Assistant template. It keeps the full docs search flow and adds one critical capability: email escalation via Resend.

Here's the flow:

  1. On startup - the agent downloads llms.txt and llms-full.txt from your docs site
  2. User asks a question - the agent searches docs locally, fetching specific pages if needed
  3. If the docs have the answer - Claude responds with a cited, accurate answer
  4. If the docs don't have the answer - the agent offers to escalate, then sends an email to your team with the full question via Resend
  5. Your team gets a clean email - with the user's question, context, and a note that the docs didn't cover it

The escalation is opt-in - the agent asks the user before sending, so you only get the questions that genuinely need human attention.

Agent Tools

ToolWhat it does
search_docsGrep through downloaded docs with keyword + context
list_doc_pagesShow full docs index from llms.txt
fetch_doc_pageFetch a specific doc page for full content
send_emailForward unanswered questions to your team via Resend

Environment Variables

VariableWhereDescription
API_KEY_21ST.env.localServer-side API key for token exchange
DOCS_URLAgent envBase URL of your docs site
RESEND_API_KEYAgent envResend API key (re_)
SUPPORT_EMAILAgent envTeam email where questions are forwarded
RESEND_FROM_EMAILAgent env (optional)Sender address, defaults to onboarding@resend.dev

Getting Started

Clone and deploy:

git clone https://github.com/21st-dev/21st-sdk-examples.git
cd 21st-sdk-examples/support-agent
npm install
npx @21st-sdk/cli login
npx @21st-sdk/cli deploy

Set Environment Variables

npx @21st-sdk/cli env set support-agent DOCS_URL https://docs.example.com
npx @21st-sdk/cli env set support-agent RESEND_API_KEY re_your_key_here
npx @21st-sdk/cli env set support-agent SUPPORT_EMAIL team@yourcompany.com
npx @21st-sdk/cli env set support-agent RESEND_FROM_EMAIL "Support <support@yourcompany.com>"

Redeploy after adding the variables:

npx @21st-sdk/cli deploy

Run the Frontend

cp .env.example .env.local
# Add your API_KEY_21ST to .env.local
npm run dev

Set Up Resend

  1. Sign up at resend.com
  2. Create an API key in the Resend dashboard
  3. Add RESEND_API_KEY and SUPPORT_EMAIL as environment variables
  4. Redeploy

If you want a custom sender domain, add and verify it in Resend's dashboard, then set RESEND_FROM_EMAIL.

What the Escalation Email Looks Like

When the agent escalates, your team receives:

  • The user's original question
  • A note that the docs didn't contain an answer
  • A request for follow-up

It's a lightweight signal that helps you identify gaps in your documentation and reach out to users who need help - before they give up.

Build Your Own

Fork this template, change the email format, add a Slack notification on escalation, or wire it into your support ticket system. The 21st SDK makes it straightforward to extend the agent with new tools.

Check the full source on GitHub and deploy your own with 21st SDK.

Agent Template

The full agent source - set DOCS_URL, RESEND_API_KEY, and SUPPORT_EMAIL, then deploy.

agents/support-agent/index.tsView on GitHub →
import { agent, tool, Sandbox } from "@21st-sdk/agent"
import { z } from "zod"

export default agent({
  runtime: "claude-code",
  model: "claude-sonnet-4-6",
  permissionMode: "bypassPermissions",
  maxTurns: 25,

  systemPrompt: `You are a support agent. Answer questions using the product
documentation. When you cannot find an answer, offer to forward via email.

ESCALATION - when to use send_email:
- You searched thoroughly and could not find a relevant answer.
- The user explicitly asks to contact the team.
- The question involves account-specific issues or billing.

WORKFLOW:
1. Grep /workspace/llms.txt to find relevant pages.
2. Grep /workspace/llms-full.txt for detailed content.
3. Use fetch_doc_page if you need full page content.
4. If no answer found, ask to forward the question - then use send_email.`,

  sandbox: Sandbox({
    setup: [
      `mkdir -p /home/user/workspace && cd /home/user/workspace && \
if [ -n "$DOCS_URL" ]; then \
  BASE=$(echo "$DOCS_URL" | sed 's|/$||'); \
  curl -fsSL "$BASE/llms.txt" -o llms.txt 2>/dev/null || echo "# Could not fetch" > llms.txt; \
  curl -fsSL "$BASE/llms-full.txt" -o llms-full.txt 2>/dev/null || true; \
else \
  echo "# No DOCS_URL configured." > llms.txt; \
fi`,
    ],
  }),

  tools: {
    fetch_doc_page: tool({
      description: "Fetch a specific documentation page by URL.",
      inputSchema: z.object({ url: z.string().url() }),
      execute: async ({ url }) => {
        const res = await fetch(url, { signal: AbortSignal.timeout(15_000) })
        const text = await res.text()
        return { content: [{ type: "text", text: text.slice(0, 30_000) }] }
      },
    }),

    search_docs: tool({
      description: "Search local documentation files for a keyword.",
      inputSchema: z.object({
        query: z.string().min(1),
        file: z.enum(["llms.txt", "llms-full.txt"]).optional().default("llms-full.txt"),
      }),
      execute: async ({ query, file }) => {
        const { execSync } = await import("child_process")
        const result = execSync(
          `grep -i -n -C 5 ${JSON.stringify(query)} /home/user/workspace/${file} | head -200`,
          { encoding: "utf-8", timeout: 10_000 },
        )
        return { content: [{ type: "text", text: result || "No matches found." }] }
      },
    }),

    send_email: tool({
      description: "Escalate to support team via Resend. Use after confirming with the user.",
      inputSchema: z.object({
        subject: z.string().min(1),
        message: z.string().min(1),
        user_email: z.string().email().optional(),
      }),
      execute: async ({ subject, message, user_email }) => {
        const apiKey = process.env.RESEND_API_KEY
        const to = process.env.SUPPORT_EMAIL
        if (!apiKey || !to) {
          return { content: [{ type: "text", text: "RESEND_API_KEY or SUPPORT_EMAIL not configured." }], isError: true }
        }
        const res = await fetch("https://api.resend.com/emails", {
          method: "POST",
          headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
          body: JSON.stringify({
            from: process.env.RESEND_FROM_EMAIL || "onboarding@resend.dev",
            to: [to],
            subject: `[Support Agent] ${subject}`,
            text: user_email ? `${message}\n\n---\nUser: ${user_email}` : message,
            ...(user_email && { reply_to: user_email }),
          }),
          signal: AbortSignal.timeout(15_000),
        })
        const data = await res.json()
        return { content: [{ type: "text", text: res.ok ? `Email sent (id: ${data.id})` : "Failed to send." }] }
      },
    }),
  },
})
Support Agent: Answer From Docs, Escalate via Email | 21st | 21st