HCC Docs

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

ReasonMeaning
queue_overflowNobody answered within maxWait
queue_closedNo rep with the skills is logged in
queue_outside_hoursThe queue's business hours turned the caller away
rep_unavailableThe rep isn't available
no_answerThe 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.
  • reason on reps.setState is 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.changed and disposition.recorded (a rep's disposition during wrap-up, also written to calls.disposition).
  • Testing: expect(step).toTransferTo(queue('sales')), then await t.queueAnswered({ repId: 'rep_1' }) or await t.transferFailed('queue_overflow').