> ## Documentation Index
> Fetch the complete documentation index at: https://developers.scoutworks.app/llms.txt
> Use this file to discover all available pages before exploring further.

# District totals in Google Sheets

> Every group in your district with its members, waiting list and growth, refreshed each morning. For district and area admins.

This recipe adds a **Groups** tab to a Google Sheet: one row per group that shares data with your district, with its members, waiting list and 12-month growth. A timer refreshes it every morning. It works the same for a county, region or the national team.

You need a **district or area key** with `nodes:structure:read` and `nodes:metrics:read`. A district admin creates one in the district's **Settings → Integrations → API keys**. See [Keys for districts and above](https://developers.scoutworks.app/guides/districts.md).

## 1. Add the script

In your Google Sheet, choose **Extensions → Apps Script**, delete what is there, paste this and choose **Save**.

```javascript Code.gs theme={null}
// Scoutworks → Google Sheets, for a district. https://developers.scoutworks.app/recipes/district-totals

function importDistrictTotals() {
  const apiKey = PropertiesService.getScriptProperties().getProperty('SCOUTWORKS_API_KEY');
  if (!apiKey) throw new Error('Add SCOUTWORKS_API_KEY in Project Settings → Script Properties.');

  // The groups and nodes below yours that share data with you.
  const names = new Map();
  walk('https://api.scoutworks.app/api/v1/nodes/descendants?limit=200', apiKey, function (n) {
    names.set(n.id, { name: n.name, tier: n.tier });
  });

  // Last night's totals: one row per group and figure.
  const figures = new Map();
  let day = '';
  walk('https://api.scoutworks.app/api/v1/nodes/metrics?limit=200', apiKey, function (m) {
    if (!figures.has(m.groupId)) figures.set(m.groupId, {});
    figures.get(m.groupId)[m.metric] = m.stale ? '' : m.value;
    day = m.day;
  });

  const rows = [['Name', 'Level', 'Members', 'Waiting', 'Growth (12 months)']];
  names.forEach(function (n, id) {
    const f = figures.get(id) || {};
    rows.push([safe(n.name), n.tier, f.member_count, f.waiting_count, f.net_growth_12m]);
  });

  const book = SpreadsheetApp.getActive();
  const sheet = book.getSheetByName('Groups') || book.insertSheet('Groups');
  sheet.clearContents();
  sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
  sheet.getRange(rows.length + 2, 1).setValue('Totals from the night of ' + day);
}

// Reads every page of a list.
function walk(url, apiKey, each) {
  let cursor = null;
  do {
    const body = fetchScoutworks(url + (cursor ? '&cursor=' + encodeURIComponent(cursor) : ''), apiKey);
    body.data.forEach(each);
    cursor = body.nextCursor;
  } while (cursor);
}

// Calls the API. Waits and tries again if the rate limit is reached.
function fetchScoutworks(url, apiKey) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = UrlFetchApp.fetch(url, {
      headers: { Authorization: 'Bearer ' + apiKey },
      muteHttpExceptions: true,
    });
    const body = JSON.parse(res.getContentText());
    if (res.getResponseCode() === 200) return body;
    if (res.getResponseCode() === 429) {
      Utilities.sleep((body.retryAfter || 30) * 1000);
      continue;
    }
    throw new Error(body.error.code + ': ' + body.error.message);
  }
  throw new Error('Scoutworks is still busy. Try again in a minute.');
}

// Stops a name that starts with = + - or @ from running as a formula.
function safe(value) {
  const text = String(value);
  return /^\s*[=+\-@]/.test(text) ? "'" + text : text;
}
```

## 2. Add your key

In the script editor, choose **Project Settings** (the cog), then **Script Properties → Add script property**. Property: `SCOUTWORKS_API_KEY`. Value: your district or area key. Choose **Save script properties**.

## 3. Run it, then refresh it every morning

Choose **importDistrictTotals** and press **Run**. Then choose **Triggers** (the clock) → **Add Trigger**: function `importDistrictTotals`, **Time-driven**, **Day timer**, **6am to 7am**.

The totals are built overnight, so a morning refresh always reads last night's. A group that stops sharing with you drops off the sheet at once, and out of your district's own total within 24 hours.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.