HCC Docs

Business hours

A queue can say when it takes new callers. Hours are per queue, with their own time zone, so they don't move if the workspace's reporting zone changes.

import {
  defineCallFlow,
  defineQueue,
  isOpen,
  nextChange,
  number,
  queue,
  TransferFailed,
} from '@hcc/sdk';

export const sales = defineQueue('sales', {
  maxWait: '2m',
  overflow: number('+14155550100'),
  hours: {
    timezone: 'America/New_York', // required: UTC, or a name such as Asia/Tokyo
    weekly: {
      mon: [['09:00', '17:00']],
      tue: [['09:00', '17:00']],
      wed: [
        ['09:00', '12:00'],
        ['13:00', '17:00'],
      ],
      thu: [['09:00', '17:00']],
      fri: [['09:00', '17:00']],
      // A day left out is closed.
    },
    exceptions: [
      { date: '2026-12-25', closed: true, name: 'Christmas' },
      { date: '2026-12-24', open: [['09:00', '13:00']] },
    ],
    atClose: 'drain', // or 'turn_away'
  },
});

// Hours only declare the schedule. What a closed queue does is your code:
export const inbound = defineCallFlow('inbound', {}, function* (c) {
  try {
    yield* c.transfer(queue(sales));
  } catch (error) {
    if (!(error instanceof TransferFailed) || error.reason !== 'queue_outside_hours') throw error;
    yield* c.say('We are closed. Please leave a message.');
    yield* c.record({ maxLengthSeconds: 120 });
    yield* c.hangup();
  }
});

isOpen(sales); // now; isOpen(sales, someDate) for another moment
nextChange(sales); // a Date, or null: no hours, or nothing changes for 14 days
  • Times are HH:MM in local time. 24:00 is allowed only as an end, and a range never crosses midnight: overnight is ['22:00', '24:00'] on one day and ['00:00', '02:00'] on the next. Each range includes its start and excludes its end.
  • Exceptions replace that whole local date, either closed: true or open ranges, at most 366. Generate the list with any holiday library. To close suddenly, add a closed exception for today and deploy.
  • Outside hours, a transfer fails with queue_outside_hours. The order is your catch, then the Default flow, which connects the number's default agent or, with none, says "We're unable to take your call right now." and hangs up. A flow can check isOpen(sales) first and skip the moment of hold music.
  • overflow doesn't run for a closed queue. It's for a caller who waited too long, so a number overflow won't ring someone's mobile at 3am. For a real 24-hour line, transfer to it yourself in onTransferFailed.
  • At closing, callers already waiting keep waiting up to maxWait, and Reps can still answer (atClose: 'drain', the default). With atClose: 'turn_away' they leave at closing, down the same queue_outside_hours path.
  • Reports count a call turned away as Outside hours, and leave it out of service level, abandonment and overflow. queue.call.abandoned carries reason: 'outside_hours'.
  • Daylight saving needs nothing from you: hours follow the local clock. An opening time inside the spring gap opens at the jump. A closing time inside the autumn repeat closes, opens again, and closes again.
  • Hours don't change a Rep's state. Outbound calls and direct rep(...) transfers aren't affected.
  • Testing: await t.transferFailed('queue_outside_hours'). To try it on a test call, deploy to test with a closed exception for today.