appscript.dev
Automation Intermediate Sheets

Generate product descriptions in bulk

Write SEO copy for an entire Northwind catalogue from a Sheet of names + specs.

Published Jul 22, 2025

A new Northwind catalogue arrives as a spreadsheet of product names and dry specs, and someone has to turn each row into a description that reads well and helps the page rank. Done by hand it is a slow, repetitive afternoon, and the later entries always read more tired than the first.

This script does the whole column at once. It walks the catalogue Sheet, and for every product still missing a description it asks Claude to write a tight, 40-word blurb from the name and specs. The tone is fixed in the prompt — for Northwind, confident and free of hype — so the hundredth description is as crisp as the first.

What you’ll need

  • A Google Sheet with your catalogue: a header row, a name column, a specs column, and an empty description column for the output.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.

The script

// The Sheet that holds the product catalogue.
const CATALOGUE_SHEET_ID = '1abcCatalogueId';

// Target length and tone for each description — change once, here.
const DESCRIPTION_WORDS = 40;
const DESCRIPTION_TONE = 'confident, no hype';

/**
 * Writes a description for every catalogue row that has a name but
 * no description yet.
 */
function generateDescriptions() {
  // 1. Read the catalogue and map header names to column indexes.
  const sheet = SpreadsheetApp.openById(CATALOGUE_SHEET_ID).getSheets()[0];
  const [h, ...rows] = sheet.getDataRange().getValues();
  if (!rows.length) {
    Logger.log('Catalogue is empty — nothing to describe.');
    return;
  }
  const col = Object.fromEntries(h.map((k, i) => [k, i]));

  // 2. Walk the rows; describe each product that needs it.
  let written = 0;
  rows.forEach((r, i) => {
    // Skip rows already described, or with no name to work from.
    if (r[col.description] || !r[col.name]) return;

    // 3. Build the prompt from the name, specs, length and tone.
    const prompt =
      'Write a ' + DESCRIPTION_WORDS + '-word product description for: ' +
      r[col.name] + '. Specs: ' + r[col.specs] + '. ' +
      'Tone: ' + DESCRIPTION_TONE + '.';

    // 4. Write the result back into the description column.
    sheet.getRange(i + 2, col.description + 1).setValue(callClaude(prompt));
    written++;
  });
  Logger.log('Wrote ' + written + ' descriptions.');
}

/**
 * Minimal Anthropic API call. The key lives in Script Properties — it
 * is never pasted into the code.
 */
function callClaude(prompt) {
  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: 'claude-haiku-4-5-20251001',
      max_tokens: 200,
      messages: [{ role: 'user', content: prompt }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. generateDescriptions opens the catalogue Sheet and reads every row, then builds a col map from header names to indexes so the code stays readable.
  2. If the Sheet has no data rows it logs that and stops before any API calls.
  3. For each row it skips anything already described or missing a name, so a re-run only fills the gaps and never overwrites finished copy.
  4. It assembles the prompt from the product name, its specs, and the shared DESCRIPTION_WORDS and DESCRIPTION_TONE constants — change those once and every description follows.
  5. Claude Haiku writes the blurb. Haiku is the right call here: short marketing copy is fast, cheap work that does not need Sonnet’s reasoning.
  6. The result is written back into the description column at i + 2 to account for the header row and zero-based loop index.

Example run

Suppose the catalogue Sheet holds:

namespecsdescription
Northwind Drafting LampLED, 5 brightness levels, USB-C, matte black(blank)
Northwind Desk Mat90x40cm, cork base, water-resistant felt top(blank)

After a run the description column is filled with copy like:

namedescription
Northwind Drafting LampA focused LED work light with five brightness levels and USB-C power. The matte-black finish sits quietly on any desk while the adjustable arm puts light exactly where you need it.
Northwind Desk MatA generous 90x40cm work surface with a water-resistant felt top and a cork base that grips. It protects your desk, softens your keyboard, and tidies the whole setup.

A column of specs becomes a column of ready-to-publish copy.

Run it

This is an on-demand job — run it when a new catalogue lands:

  1. In the Apps Script editor, select generateDescriptions and click Run.
  2. Approve the authorisation prompt the first time.
  3. Open the Sheet and review the descriptions before publishing.

To let the catalogue team trigger it from the Sheet, add a custom menu:

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Catalogue tools')
    .addItem('Generate descriptions', 'generateDescriptions')
    .addToUi();
}

Watch out for

  • A 40-word target is a guide, not a guarantee. Models land near the figure but rarely hit it exactly — if length must be precise, trim in review.
  • Descriptions are only as good as the specs. A vague or empty specs cell gives Claude little to work with and the copy turns generic.
  • Re-running skips filled rows. To regenerate a description, clear its cell first — otherwise the r[col.description] guard treats it as done.
  • Large catalogues take time and tokens. Each row is one API call; a catalogue of thousands may need batching or a higher-rate plan to finish in one run.
  • Claude does not check facts. It writes from the specs you give it — if a spec is wrong, the description will repeat the mistake confidently.

Related