Queues and reps
Reps are the people who take calls at the dashboard's desk (/desk). They are managed in the dashboard and the API, never in code. Queues are code: export them from a file hcc scans, and hcc deploy registers them. (With the lower-level API, list them on the flow, or in hcc.serve({ queues }).)
import { agent, defineCallFlow, defineQueue, queue, rep, TransferFailed } from '@hcc/sdk';
export const sales = defineQueue('sales', {
displayName: 'Sales',
skills: { english: 1, sales: 3 }, // a rep needs at least these levels (1-10)
maxWait: '3m', // then it overflows (default 5m)
overflow: agent('agt_backup'), // or queue(other), number('+1…'), 'voicemail'
holdMusic: 'https://example.com/hold.mp3', // default: the provider's music
announcePosition: '45s', // "You are number 2 in line"; default false
wrapUp: '30s', // after a call; default 0
strategy: 'longest_idle', // or 'round_robin'
});
export const inbound = defineCallFlow('inbound', {}, function* (c) {
const vip = yield* c.run('is_vip', () => crm.isVip(c.call.from));
yield* c.say('One moment.');
try {
yield* c.transfer(
vip
? rep('rep_123', { timeout: '20s' })
: queue(sales, { priority: 5, context: { leadId: 'l1' } }),
);
} catch (error) {
if (!(error instanceof TransferFailed)) throw error;
// error.reason: queue_overflow | queue_closed | queue_outside_hours | rep_unavailable | no_answer | …
if (error.target.type === 'queue') throw error; // not caught: the queue's overflow runs
yield* c.say('Sorry, nobody is available.');
yield* c.hangup();
}
});
await hcc.queues.stats('sales'); // { waiting, longestWaitSeconds, repsAvailable, repsLoggedIn }
for await (const r of hcc.reps.list()) console.log(r.id, r.state);
await hcc.reps.update('rep_123', { skills: { sales: 5 } });
await hcc.reps.setState('rep_123', 'away', { reason: 'training' });How a call waits
The caller waits in the call's conference with hold music. The waiting call is offered to one available rep at a time who has every skill: highest priority first, then the oldest call. The rep's desk rings for 20 s. A rep who lets it ring out goes away (missed_offer), and the next rep is offered the call.
context (at most 4 KB as JSON) shows on the rep's screen pop, one row per key. A value that is a whole http: or https: URL becomes a link that opens in a new tab, so a flow can look up a lead and pass { crm: 'https://crm.example.com/leads/42' }. Anything else stays text. queue(name) works with a name too, if the queue is registered elsewhere.
c.connect(queue | rep) is refused: queues and reps are reached by c.transfer.
When nobody takes it
| Reason | Meaning |
|---|---|
queue_overflow | Nobody answered within maxWait |
queue_closed | No rep with the skills is logged in |
queue_outside_hours | The queue's business hours turned the caller away |
rep_unavailable | The rep isn't available |
no_answer | The rep let it ring out |
The order is the same as any transfer: your catch, then the queue's overflow, then the Default flow. queue_outside_hours skips the queue's overflow, which is for a caller who waited too long: your code decides in the catch.
Rep presence
A rep on the browser softphone needs the desk open. With no live desk for about 3 minutes, they go offline with reason desk_closed (rep.state.changed), and they stay Offline until they or your code set them back. In the meantime no call or campaign lead goes to a desk known to be gone.
- A short network drop or a tab in the background doesn't count. Reps on their own phone, and reps on a call, aren't affected.
reps.setState('rep_123', 'available')on a softphone rep with no desk open lasts the same 3 minutes.reasononreps.setStateis free text (at most 64 characters). It isn't checked against the Away reasons reps pick from on the desk, which owners and admins edit in Settings.
Events and tests
- Events:
queue.call.entered,queue.call.answered,queue.call.abandoned,rep.state.changedanddisposition.recorded(a rep's disposition during wrap-up, also written tocalls.disposition). - Testing:
expect(step).toTransferTo(queue('sales')), thenawait t.queueAnswered({ repId: 'rep_1' })orawait t.transferFailed('queue_overflow').