Transfers
The flow moves the caller to someone else with c.transfer. A tool asks for it by returning handoff, and code outside the flow by sending a custom event. transfer is blind: whoever is connected (the agent, a dialed party) leaves, the target joins, and the caller stays on.
import { agent, defineCallFlow, handoff, number, sip, tool, TransferFailed } from '@hcc/sdk';
import { z } from 'zod';
export const route = defineCallFlow(
'route',
{
tools: {
wants_human: tool({
description: 'The caller wants a person.',
input: z.object({}),
afterSpeech: true,
handler: () => handoff('human'),
}),
to_closer: tool({
description: 'Hand over to a closer.',
input: z.object({}),
handler: () => handoff('closer'),
}),
book: tool({
description: 'Book a slot.',
input: z.object({ slot: z.string() }),
handler: () => ({ reply: 'Booked' }),
}),
},
agents: {
qualifier: agent('agt_qualifier', { tools: ['wants_human', 'to_closer'] }),
closer: agent('agt_closer', { tools: ['book'] }),
},
handoffs: ['human', 'closer'],
events: ['escalate'],
},
function* (c) {
const session = yield* c.connect('qualifier', { until: ['escalate'] });
try {
if (session.interrupted) {
yield* c.transfer(sip('sip:desk@pbx.example.com'));
} else if (session.handoff === 'closer') {
yield* c.transfer('closer'); // connects the closer with its own bound tools
} else if (session.handoff === 'human') {
yield* c.say('Connecting you now.');
yield* c.transfer(number('+15550100003'), {
timeout: '25s', // ring time before it fails with no_answer
callerId: '+15550100009', // defaults to the call's number
});
} else {
yield* c.hangup();
}
} catch (error) {
if (!(error instanceof TransferFailed)) throw error;
// error.reason: busy | no_answer | failed | unavailable; error.target: { type, agentId | to | uri }
yield* c.say('Nobody is available right now.');
yield* c.hangup({ disposition: `transfer_${error.reason}` });
}
},
);
await hcc.calls.emit(callId, 'escalate'); // from your CRM: transfer a running call- The caller is never left alone. If the target is busy, doesn't answer or can't be reached, the caller waits while your
catchdecides. If you don't catch it, the call's Default flow runs (its default agent, or an apology). - Events:
call.transfer.started,call.transfer.completedandcall.transfer.failed, plus thecall.participant.*Events of whoever joins and leaves (reason: handoff). - A transfer that works never comes back, so nothing after it runs. Use
c.connect(number | sip)for a plain bridge: the code after it runs when the bridge ends or nobody answers. - Calls only grow into a conference when they bridge to a number or a SIP endpoint. A call that just talks to a stream agent never gets one.
- There is no REST transfer endpoint:
hcc.calls.emitis how code outside the flow transfers a call, and only while the flow is at ac.gatherorc.connectwhoseuntilnames the event. Once the caller is in a queue or with a person, the event just waits in the inbox. A warm transfer from a Rep is different: see Warm transfers.