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 '@hccrun/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'
  language: { prefer: 30 }, // wait up to 30 s for a rep who speaks the caller's language; default 0
});

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.update('rep_123', { languages: ['es', 'en'] }); // what they speak on calls, main first
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.

Language routing

A rep speaks a list of languages, main one first (default ['en']). With language: { prefer: 30 }, a caller whose language is known is offered only to reps who speak it for the first 30 s of the wait. After that any rep with the skills takes it, speakers still first, and live translation covers the gap.

  • The caller's language is known from the lead (preferredLanguage), c.setLanguage('es'), the queue's translation.customer, or queue(sales, { language: 'es' }) for one transfer. A detected language never counts: detection needs a rep on the call.
  • Tags compare on the base language: a rep with es takes an es-MX caller.
  • If no logged-in rep who could take the call speaks the language, the call doesn't wait.
  • The window counts from when the caller got in line. An offer that rings out doesn't restart it.
  • Only inbound queue calls. Campaign dials ignore it.
  • A rep who speaks the caller's language gets no translation. For anyone else it translates into their main language.

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').