> ## 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.

# A weekly numbers email

> Every Monday, an email tells your leaders how many young people are in each section, who has joined the waiting list, and who is waiting for a reply.

This recipe sends one short email each week. It answers the questions a group lead volunteer asks most:

* How many young people are in each section?
* How many are on the waiting list for each section?
* Who applied this week?
* Which applications still wait for a first reply?

It uses Google Apps Script and your own Gmail account. It takes about ten minutes.

## 1. Add the script

Go to [script.google.com](https://script.google.com) and choose **New project**. Delete what is there, paste the script below and choose **Save**.

```javascript Code.gs theme={null}
// Scoutworks → weekly numbers email. https://developers.scoutworks.app/recipes/weekly-numbers

const BASE = 'https://api-demo.scoutworks.app/api/v1';
const WAITING = ['received', 'contacted', 'offered', 'accepted'];
const DAY = 24 * 60 * 60 * 1000;

function sendWeeklyNumbers() {
  const to = PropertiesService.getScriptProperties().getProperty('SEND_TO') ||
    Session.getEffectiveUser().getEmail();
  const group = fetchScoutworks(BASE + '/me').data.group.name;
  const members = readAll('/members');
  const waiting = readAll('/waiting-list').filter(function (w) {
    return WAITING.indexOf(w.status) !== -1;
  });
  const weekAgo = Date.now() - 7 * DAY;

  // Members and waiting list, side by side, for each section.
  const sections = {};
  function row(name) {
    return (sections[name] = sections[name] || { members: 0, waiting: 0 });
  }
  members.forEach(function (m) { if (m.sections[0]) row(m.sections[0].name).members++; });
  waiting.forEach(function (w) { row(w.targetSection ? w.targetSection.name : 'No section').waiting++; });

  const table = Object.keys(sections).sort().map(function (name) {
    return '<tr><td>' + html(name) + '</td><td align="right">' + sections[name].members +
      '</td><td align="right">' + sections[name].waiting + '</td></tr>';
  }).join('');

  const newThisWeek = waiting.filter(function (w) { return new Date(w.appliedAt).getTime() >= weekAgo; });
  const noReply = waiting.filter(function (w) {
    return w.status === 'received' && new Date(w.appliedAt).getTime() < weekAgo;
  });

  const body =
    '<h2>' + html(group) + ': this week</h2>' +
    '<table cellpadding="4"><tr><th align="left">Section</th><th>Members</th><th>Waiting</th></tr>' +
    table + '</table>' +
    '<h3>New on the waiting list (' + newThisWeek.length + ')</h3>' + list(newThisWeek) +
    '<h3>Waiting more than a week for a first reply (' + noReply.length + ')</h3>' + list(noReply);

  MailApp.sendEmail({
    to: to,
    subject: group + ': ' + members.length + ' members, ' + waiting.length + ' waiting',
    htmlBody: body,
  });
}

// A bullet list of applications, or a short note if there are none.
function list(entries) {
  if (entries.length === 0) return '<p>None.</p>';
  return '<ul>' + entries.map(function (w) {
    return '<li>' + html(w.firstName + ' ' + w.lastName) + ' for ' +
      html(w.targetSection ? w.targetSection.name : 'no section') +
      ', applied ' + w.appliedAt.slice(0, 10) + '</li>';
  }).join('') + '</ul>';
}

// Names on the waiting list come from a public form. Escape them before they go into HTML.
function html(value) {
  return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

// Every item in a list, following nextCursor to the last page.
function readAll(path) {
  const items = [];
  let cursor = null;
  do {
    let url = BASE + path + (path.indexOf('?') === -1 ? '?' : '&') + 'limit=200';
    if (cursor) url += '&cursor=' + encodeURIComponent(cursor);
    const body = fetchScoutworks(url);
    items.push.apply(items, body.data);
    cursor = body.nextCursor;
  } while (cursor);
  return items;
}

// Calls the API. Waits and tries again if the rate limit is reached.
function fetchScoutworks(url) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('SCOUTWORKS_API_KEY');
  if (!apiKey) throw new Error('Add SCOUTWORKS_API_KEY in Project Settings → Script Properties.');
  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.');
}
```

<Warning>
  Keep the `html()` function. Anyone can type a name into the public waiting-list form. Without `html()`, a name could put a link or an image into your email.
</Warning>

## 2. Add your key and the address

In the script editor, choose **Project Settings** (the cog), then **Script Properties**. Add two properties:

| Property             | Value                                                                                                      |
| -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `SCOUTWORKS_API_KEY` | The [sandbox key](/guides/sandbox) to try it, or your own key.                                             |
| `SEND_TO`            | Who gets the email. Put a comma between two or more addresses. If you leave it out, the email goes to you. |

## 3. Run it once

Choose **sendWeeklyNumbers** in the toolbar and press **Run**. Google asks for permission the first time: the script needs to reach an external service and to send email as you. Check your inbox.

## 4. Send it every Monday

Choose **Triggers** (the clock), then **Add Trigger**. Function: `sendWeeklyNumbers`. Event source: **Time-driven**. Type: **Week timer**. Day: **Every Monday**. Time: **7am to 8am**. Choose **Save**.

## 5. Switch to your group

Change `api-demo.scoutworks.app` to `api.scoutworks.app`, and replace the sandbox key with your own. Your key needs `members:read` and `waiting-list:read`. It does not need a `.contacts` scope.

## Notes

* **Who counts as waiting.** The script counts applications at `received`, `contacted`, `offered` or `accepted`. Change `WAITING` to count other stages. The [API reference](/api-reference/introduction) lists them all.
* **Each member counts once**, in their main section. A young person who is moving on is not counted twice.
* **Gmail limits.** A free Gmail account can send email to about 100 people a day from a script. A weekly email to a few leaders is far below this.
