Skip to content

A queue call ​

A caller calls a queue — support — instead of a person, and Talkline rings whichever member of the queue is free: one member at a time, never the same member twice for one call, until one answers or the queue gives up. The agents are ordinary identities with ordinary clients; nothing about them is special except that they are members.

Set up the queue ​

Queues are configured from your backend, with @talkline/server-sdk or any HTTP client (Queues reference):

ts
import { Talkline } from '@talkline/server-sdk';

const talkline = new Talkline({
  apiKey: process.env.TALKLINE_API_KEY!,
  apiSecret: process.env.TALKLINE_API_SECRET!,
  baseUrl: process.env.TALKLINE_API_URL!, // https://api.talkline.example
});

const queue = await talkline.queues.create({
  slug: 'support',
  ringTimeoutSec: 20, // how long each member rings
  maxWaitSec: 300, // give up on a caller after five minutes
  overflowAction: 'webhook', // and tell my backend when it does
});

await talkline.queues.addMember(queue.id, { identity: 'agent:7', skills: ['billing'] });
await talkline.queues.addMember(queue.id, { identity: 'agent:9', skills: ['billing', 'es'] });

A queue's routes take its id, a UUID; a call names the queue by its slug or its id. There is no route that reads a queue by its slug: talkline.queues.iterate() lists every queue, ordered by slug, and finds it.

Creating a slug that exists is refused 409 conflict, and so is adding someone who is already a member; changing or removing someone who is not is 404 identity_not_found. A setup script that may run more than once therefore looks before it creates:

ts
let support;
for await (const q of talkline.queues.iterate()) if (q.slug === 'support') support = q;
support ??= await talkline.queues.create({ slug: 'support', ringTimeoutSec: 20 });

const members = new Set<string>();
for await (const m of talkline.queues.iterateMembers(support.id)) members.add(m.identity);
if (!members.has('agent:7')) await talkline.queues.addMember(support.id, { identity: 'agent:7' });

An application has at most 200 queues, and a queue at most 1 000 members.

The settings, all optional:

SettingDefaultMeaning
strategyfair_shareHow the next member is chosen — below.
ringTimeoutSec30How long one member rings before the next is tried (5–120).
redirectDelayMs2000The pause after one member declines or rings out before the next is rung.
maxAttempts20Members rung, at most, for one call.
maxWaitSec300How long a caller waits, at most.
overflowActionfailWhen the queue gives up: fail ends the call; webhook also sends queue.overflowed.

Who is offered a call ​

A member is rung only when all of these hold:

  1. They are a member of the queue, and their membership is not paused (pausedUntil unset or past).
  2. They are online: a client of theirs is connected right now.
  3. Their availability is available — not away or dnd. It is available until they choose otherwise.
  4. They are not already in a call, unless the queue allows it (settings.allow_concurrent).
  5. They have not already been rung for this call.
  6. Their skills cover the call's required skills.

Your agents' app therefore keeps a TalklineClient connected while they are on shift, and sets their availability as they choose it — from your backend, talkline.identities.upsert(identity, { availability: 'away' }) (Identities and presence). Pausing a membership takes someone out of one queue for a while without changing their availability anywhere else:

ts
const until = new Date(Date.now() + 15 * 60_000).toISOString();
await talkline.queues.updateMember(queue.id, 'agent:7', { pausedUntil: until }); // null resumes

Strategies ​

StrategyThe next member is…
fair_sharethe lowest score of: calls completed today, a call from this caller declined today (heavily), calls in progress, priority — less time idle. Ties are broken the same way on every retry of one call
round_robinthe next in turn
longest_idlethe one idle longest
least_busythe one in the fewest calls right now
skill_prioritythe lowest priority among those with the skills, then as fair_share
pinnedalways one named member (settings.pinned_identity)

simulate ranks the members for a caller right now — who would be rung, in what order, and why each excluded member is excluded — without placing a call. explain shows one member's score and the inputs behind it:

ts
const ranking = await talkline.queues.simulate(queue.id, { caller: 'guest:9f3a', skills: 'billing,es' });
// ranking.decision: { kind: 'offer', agent: 'agent:9' }; ranking.excluded: [{ identity: 'agent:7', reason: 'skill_mismatch' }]
const why = await talkline.queues.explain(queue.id, { caller: 'guest:9f3a', agent: 'agent:7' });

The caller ​

ts
const call = await client.call({ queue: 'support', media: 'audio' });

queue takes the slug or the queue's id, instead of to. A queue your application does not have is refused protocol_error — POST /v1/calls answers 404 not_found for the same mistake. The caller is seated at once and hears nothing until a member answers. While it waits, call.queue tells it where it stands, and callUpdated fires whenever that changes:

ts
client.on('callUpdated', (call) => {
  const q = call.queue;
  if (!q) return;
  if (q.agent) showConnectedTo(q.agent); // a member answered
  else if (q.failure) showNobodyAvailable(); // the queue gave up; callEnded follows
  else showPosition(q.position, q.estimatedWaitSec); // either may be null: not known yet
});
call.queue fieldMeaning
nameThe queue, as you named it in call().
position1 is next. Null until the first update arrives.
estimatedWaitSecSeconds, or null when the server has no estimate — never 0. Today it is always null: no estimate is made yet.
agentThe member who answered.
failureWhy the queue gave up: no_agents_available. Treat a code you do not know the same way.

When a member answers, the call is a one-to-one call like any other (A one-to-one call): callStarted fires, then the member's media arrives. When the queue gives up — nobody eligible, or the queue's wait or attempts spent — the call ends with the reason no_answer.

call.cancel() leaves the queue. A member whose client is ringing at that moment stops ringing.

The member ​

A member's client receives a queue call as an ordinary incomingCall, and answers or declines it the same way. Declining, or letting it ring out, is not the end of the call: the queue rings the next member, and the caller is not told. Before it is answered, only the caller — or your backend, with POST /v1/calls/{sid}/end — can end it.

Mint members' tokens like any other caller's — roomJoin, canPublish, canSubscribe — so they can answer.

Required skills ​

A call placed by your backend can require skills:

ts
await talkline.calls.create({ from: 'guest:5d0c…', queue: 'support', requiredSkills: ['es'] });

Only members whose skills include every required skill are offered it. A call placed from a client with client.call({ queue }) has no required skills.

On your backend ​

WebhookWhen
queue.enteredThe call joined the queue.
queue.assignedA member answered: assignedIdentity, attempts.
queue.abandonedThe caller hung up while waiting.
queue.overflowedThe queue gave up and its overflowAction is webhook.

A queue that gives up ends the call no_answer, which also arrives as call.missed. talkline.queues.entries(id) lists the calls waiting and recently handled — iterateEntries pages through them all — and talkline.queues.stats(id) summarises the line now and today's calls. Queues reference.

Talkline documentation