# Slack who's away digest

Post a daily or weekly "who's away" message to Slack from HollyHR's read
endpoints. This recipe uses a Slack incoming webhook and a scheduled job that
you run outside HollyHR.

This is a custom workflow recipe, not a built-in Slack bot. The beta API cannot
book time off or approve requests. This recipe uses scheduled polling because
it reports current who's-away state; webhook events are available separately for
public API-origin writes.

## What it reads

- `GET /people`
- `GET /time-off?from=YYYY-MM-DD&to=YYYY-MM-DD&status=approved`

This recipe uses the safe `people:read` projection only. Dates of birth and
home contact details require the separate `people:personal:read` detail
endpoint and are not needed for who's-away messages. Compensation, bank details,
tax identifiers, and document contents are not exposed.

## Environment

```bash
export HOLLYHR_API_TOKEN="hhr_live_..."
export HOLLYHR_API_BASE_URL="https://{workspace}.hollyhr.com/api/v1"
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
```

## Node.js example

Run this from a server, scheduled job, or GitHub Actions workflow. Do not put
the HollyHR API token in browser code.

```js
const apiBaseUrl = process.env.HOLLYHR_API_BASE_URL;
const apiToken = process.env.HOLLYHR_API_TOKEN;
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;

if (!apiBaseUrl || !apiToken || !slackWebhookUrl) {
  throw new Error("Missing HOLLYHR_API_BASE_URL, HOLLYHR_API_TOKEN, or SLACK_WEBHOOK_URL");
}

async function holly(path) {
  const response = await fetch(`${apiBaseUrl}${path}`, {
    headers: {
      Authorization: `Bearer ${apiToken}`,
      Accept: "application/json",
    },
  });

  if (!response.ok) {
    throw new Error(`HollyHR API request failed: ${response.status} ${await response.text()}`);
  }

  return response.json();
}

async function listAll(path) {
  const items = [];
  let cursor;

  do {
    const separator = path.includes("?") ? "&" : "?";
    const page = await holly(
      `${path}${cursor ? `${separator}cursor=${encodeURIComponent(cursor)}` : ""}`,
    );
    items.push(...page.data);
    cursor = page.pagination?.next_cursor;
  } while (cursor);

  return items;
}

function isoDate(date) {
  return date.toISOString().slice(0, 10);
}

function addDays(date, days) {
  const next = new Date(date);
  next.setUTCDate(next.getUTCDate() + days);
  return next;
}

function formatLine(entry, peopleById) {
  const person = peopleById.get(entry.person_id);
  const name = person?.display_name ?? "Someone";
  const department = person?.department?.name ? ` (${person.department.name})` : "";
  const dates =
    entry.start_date === entry.end_date
      ? entry.start_date
      : `${entry.start_date} to ${entry.end_date}`;

  return `- ${name}${department}: away, ${dates}`;
}

async function main() {
  const today = new Date();
  const from = isoDate(today);
  const to = isoDate(addDays(today, 6));

  const [people, timeOff] = await Promise.all([
    listAll("/people?limit=100"),
    listAll(`/time-off?limit=100&status=approved&from=${from}&to=${to}`),
  ]);

  const peopleById = new Map(people.map((person) => [person.id, person]));
  const lines = timeOff.map((entry) => formatLine(entry, peopleById));
  const text =
    lines.length > 0
      ? `Who's away from ${from} to ${to}:\n${lines.join("\n")}`
      : `Nobody is away from ${from} to ${to}.`;

  const slackResponse = await fetch(slackWebhookUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text }),
  });

  if (!slackResponse.ok) {
    throw new Error(`Slack webhook failed: ${slackResponse.status} ${await slackResponse.text()}`);
  }
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

## Scheduling

Run the script once each weekday morning for a daily digest, or once on Monday
for a weekly digest. For external schedulers, store the HollyHR token and Slack
webhook URL as encrypted secrets.

The example posts presence only. It deliberately does not post the leave
category into Slack, because categories such as sickness can be sensitive in a
shared channel.

## Built-in alternative

For non-technical teams, HollyHR also has a built-in calendar subscription from
the Who's Away page. Use that for Google Calendar, Outlook, or Apple Calendar
when you only need a live team calendar rather than a Slack message.
