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:MMin local time.24:00is 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: trueoropenranges, at most 366. Generate the list with any holiday library. To close suddenly, add aclosedexception for today and deploy. - Outside hours, a transfer fails with
queue_outside_hours. The order is yourcatch, 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 checkisOpen(sales)first and skip the moment of hold music. overflowdoesn'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 inonTransferFailed.- At closing, callers already waiting keep waiting up to
maxWait, and Reps can still answer (atClose: 'drain', the default). WithatClose: 'turn_away'they leave at closing, down the samequeue_outside_hourspath. - Reports count a call turned away as Outside hours, and leave it out of service level, abandonment and overflow.
queue.call.abandonedcarriesreason: '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 aclosedexception for today.