HollyHR Developer Docs
  • HollyHR
  • Sign in
  • Manage API keys
  • Start Here
  • Core API
  • AI and MCP
  • API Reference
  • Recipes
  • Resources
Recipe indexGitHub Actions recipesMCP smoke testSafe MCP leave bookingPeople syncTest SDK from sourceSlack who's awayGoogle Sheets exportReceive webhooks in NodeCreate person + webhookWebhook + payroll referencesPayroll readiness export
Recipes

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

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

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

Last modified on June 24, 2026
Test SDK from sourceGoogle Sheets export
On this page
  • What it reads
  • Environment
  • Node.js example
  • Scheduling
  • Built-in alternative
Javascript