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

# Birthdays this month

> A sheet of the birthdays in each section, soonest first, and an optional email on the first of each month.

Many sections sing happy birthday at the meeting, or give a birthday badge or a card. This recipe lists the birthdays coming up, with the age each young person turns. It can also email the list to your leaders on the first of each month.

It takes about five minutes.

## 1. Add the script

In your Google Sheet, choose **Extensions → Apps Script**. Delete what is there, paste the script below and choose **Save**.

```javascript Code.gs theme={null}
// Scoutworks → birthdays. https://developers.scoutworks.app/recipes/birthdays

const BASE = 'https://api-demo.scoutworks.app/api/v1';
const DAYS_AHEAD = 31;

// Fills the Birthdays tab. Returns the rows, so the email can use them too.
function birthdays() {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const rows = [];

  const members = readAll('/members');
  // A key without members.contacts:read gets no dateOfBirth field at all.
  if (members.length && !('dateOfBirth' in members[0])) {
    throw new Error('This key needs the members.contacts:read scope to read dates of birth.');
  }

  members.forEach(function (m) {
    if (!m.dateOfBirth) return; // No date of birth recorded.
    const parts = m.dateOfBirth.split('-').map(Number);
    let next = new Date(today.getFullYear(), parts[1] - 1, parts[2]);
    if (next < today) next = new Date(today.getFullYear() + 1, parts[1] - 1, parts[2]);
    const days = Math.round((next - today) / 86400000);
    if (days > DAYS_AHEAD) return;

    rows.push([
      Utilities.formatDate(next, Session.getScriptTimeZone(), 'EEE d MMM'),
      safe(m.preferredName || m.firstName),
      safe(m.lastName),
      next.getFullYear() - parts[0],
      safe(m.sections[0] ? m.sections[0].name : ''),
      days,
    ]);
  });

  rows.sort(function (a, b) { return a[5] - b[5]; });
  const table = [['Birthday', 'First name', 'Last name', 'Turns', 'Section', 'Days away']].concat(rows);
  const book = SpreadsheetApp.getActive();
  const sheet = book.getSheetByName('Birthdays') || book.insertSheet('Birthdays');
  sheet.clearContents();
  sheet.getRange(1, 1, table.length, table[0].length).setValues(table);
  sheet.setFrozenRows(1);
  return rows;
}

// Optional: emails the list. Run it from a monthly trigger.
function emailBirthdays() {
  const to = PropertiesService.getScriptProperties().getProperty('SEND_TO') ||
    Session.getEffectiveUser().getEmail();
  const lines = birthdays().map(function (r) {
    return r[0] + ': ' + r[1] + ' ' + r[2] + ' turns ' + r[3] + ' (' + r[4] + ')';
  });
  MailApp.sendEmail(to, 'Birthdays in the next ' + DAYS_AHEAD + ' days',
    lines.length ? lines.join('\n') : 'No birthdays this month.');
}

// 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.');
}

// 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: the [sandbox key](/guides/sandbox) to try it, or your own key.

To send the email to other leaders, add a second property, `SEND_TO`, with their addresses. Put a comma between them. Without it, the email goes to you.

## 3. Run it

Choose **birthdays** in the toolbar and press **Run**. Open the **Birthdays** tab.

To get the email too, run **emailBirthdays** once, so Google asks for permission to send email. Then add a trigger: choose **Triggers** (the clock), then **Add Trigger**. Function: `emailBirthdays`. Event source: **Time-driven**. Type: **Month timer**. Day: **1st**. Choose **Save**.

## 4. 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 `members.contacts:read`. The second scope adds dates of birth.

<Warning>
  Dates of birth are personal data. Keep the sheet private to your leaders, and send the email only to leaders who need it.
</Warning>

## Change it

* **One section.** Change `readAll('/members')` to `readAll('/members?sectionId=…')` with the section’s `id`. Get the `id` from any member in the [members list](/recipes/google-sheets).
* **Further ahead.** Change `DAYS_AHEAD`. For example, `7` gives this week only, for a weekly email.
* **A birthday on 29 February.** In a year with no 29 February, the script shows the birthday on 1 March.
