Appearance
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:
| Setting | Default | Meaning |
|---|---|---|
strategy | fair_share | How the next member is chosen — below. |
ringTimeoutSec | 30 | How long one member rings before the next is tried (5–120). |
redirectDelayMs | 2000 | The pause after one member declines or rings out before the next is rung. |
maxAttempts | 20 | Members rung, at most, for one call. |
maxWaitSec | 300 | How long a caller waits, at most. |
overflowAction | fail | When 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:
- They are a member of the queue, and their membership is not paused (
pausedUntilunset or past). - They are online: a client of theirs is connected right now.
- Their availability is
available— notawayordnd. It isavailableuntil they choose otherwise. - They are not already in a call, unless the queue allows it (
settings.allow_concurrent). - They have not already been rung for this call.
- 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 resumesStrategies
| Strategy | The next member is… |
|---|---|
fair_share | the 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_robin | the next in turn |
longest_idle | the one idle longest |
least_busy | the one in the fewest calls right now |
skill_priority | the lowest priority among those with the skills, then as fair_share |
pinned | always 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 field | Meaning |
|---|---|
name | The queue, as you named it in call(). |
position | 1 is next. Null until the first update arrives. |
estimatedWaitSec | Seconds, or null when the server has no estimate — never 0. Today it is always null: no estimate is made yet. |
agent | The member who answered. |
failure | Why 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
| Webhook | When |
|---|---|
queue.entered | The call joined the queue. |
queue.assigned | A member answered: assignedIdentity, attempts. |
queue.abandoned | The caller hung up while waiting. |
queue.overflowed | The 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.