appscript.dev
Automation Intermediate Gmail

Reply to customers in their own language

Build a multilingual Northwind response generator — same support voice, native phrasing.

Published Oct 10, 2025

Northwind sells worldwide, but its support team writes in English. A question in French or Japanese either waits for someone who can answer it, or gets a machine-translated reply that reads like a machine wrote it. Neither is the warm, on-brand response a customer expects.

This script closes the gap. It scans unread support email, detects the language each customer wrote in, and asks Claude to draft a reply in that same language — keeping Northwind’s friendly, concise support voice. The draft lands in the thread for an agent to skim and send. Nobody on the team needs to speak the language; they just need to trust the draft enough to hit send.

What you’ll need

  • A Gmail account where support mail carries a support label — a filter that applies it on arrival is the easiest way.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.
  • Nothing else — the script creates its own support/multi-lang label to mark threads it has already handled.

The script

// Only threads with this label are considered for a draft.
const SUPPORT_LABEL = 'support';

// Applied once a draft exists, so the same thread is never drafted twice.
const DONE_LABEL = 'support/multi-lang';

// Keep replies short — agents skim, customers appreciate brevity.
const MAX_WORDS = 120;

/**
 * Finds unread support threads, detects the customer's language, and
 * drafts a reply in that language for an agent to review and send.
 */
function multilingualDraft() {
  // 1. Pull unread support threads that have not been drafted yet.
  const query = `label:${SUPPORT_LABEL} is:unread -label:${DONE_LABEL}`;
  const threads = GmailApp.search(query);

  if (!threads.length) {
    Logger.log('No unread support threads — nothing to draft.');
    return;
  }

  // 2. Get (or create) the label that marks a thread as handled.
  const tag = GmailApp.getUserLabelByName(DONE_LABEL)
    || GmailApp.createLabel(DONE_LABEL);

  for (const t of threads) {
    // 3. Read the first message — the customer's original question.
    const msg = t.getMessages()[0];
    const body = msg.getPlainBody();

    // 4. Detect the language; fall back to English if detection fails.
    const lang = LanguageApp.detect(body) || 'en';

    // 5. Ask Claude for a reply in that language, in Northwind's voice.
    const prompt =
      `Reply in ${lang} to this Northwind support question. ` +
      `Be friendly and concise (≤${MAX_WORDS} words). ` +
      `Write only the reply — no greeting line about the language.\n\n` +
      body;
    const reply = callClaude(prompt, 'claude-sonnet-4-6');

    // 6. Attach the draft to the thread and mark it handled.
    t.createDraftReply(reply);
    t.addLabel(tag);
  }
  Logger.log('Drafted replies for ' + threads.length + ' thread(s).');
}

/**
 * Minimal Anthropic API call. The key lives in Script Properties — it
 * is never pasted into the code.
 */
function callClaude(prompt, model = 'claude-haiku-4-5-20251001', maxTokens = 400) {
  const key = PropertiesService.getScriptProperties()
    .getProperty('ANTHROPIC_API_KEY');
  const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: prompt }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. multilingualDraft searches Gmail for unread threads labelled support that do not yet carry the support/multi-lang label — the threads still waiting for a reply.
  2. If the search comes back empty, it logs a message and stops — no wasted work.
  3. It fetches the support/multi-lang label, creating it on the first run.
  4. For each thread it reads the first message, the customer’s original question.
  5. LanguageApp.detect returns the language code — fr, ja, de — and falls back to en if it cannot tell.
  6. It asks Claude Sonnet to draft a reply in that language, keeping it friendly and under MAX_WORDS. Sonnet is worth the cost here: phrasing a reply that sounds native, not translated, needs more than a cheap model.
  7. createDraftReply attaches the draft to the thread, and the label marks it handled so the next run skips it.

Example run

Say an unread support thread contains this customer message:

Bonjour, je n’arrive pas à réinitialiser mon mot de passe. Le lien dans l’email ne fonctionne pas. Pouvez-vous m’aider ?

LanguageApp.detect returns fr, and Claude drafts a reply in French:

Bonjour, merci de nous avoir contactés. Le lien de réinitialisation expire après 30 minutes — demandez-en un nouveau depuis la page de connexion et utilisez-le aussitôt. Si le problème persiste, répondez à cet email et nous réinitialiserons votre mot de passe manuellement. Bonne journée !

The draft sits in the thread, and the agent sends it after a quick read — no French required on the team’s side.

Trigger it

Run this on a schedule so drafts are waiting before anyone opens the inbox:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Add a trigger for multilingualDraft, Time-driven, every 15 minutes.
  3. Approve the authorisation prompt the first time it runs.

A short interval keeps drafts fresh without hammering the API — the -label:support/multi-lang filter means each run only touches new threads.

Watch out for

  • These are drafts, not sent replies. Always keep an agent in the loop — they catch the rare case where Claude misreads the question or the detected language is wrong.
  • LanguageApp.detect struggles with very short messages. A one-line “merci?” may detect as English; the fallback to en keeps the script running, but the reply language can miss.
  • The script only reads the first message in the thread. For a long back-and-forth it will not see later context — fine for a fresh question, less so for a reply deep in an existing thread.
  • API calls run inside the loop, so a large backlog can hit Apps Script’s six-minute limit. If the inbox is busy, lower the schedule interval so each run has fewer threads to clear.
  • Detection and drafting both cost a model call’s worth of latency per thread. For very high volume, batch the language detection or cache replies to common questions instead of drafting every one fresh.

Related