Skip to main content
An outbound agent starts the call. That changes two things: the channel has to declare it, and the agent usually needs to know who it is calling before the first word. No shipped example dials out: examples/salon-concierge declares outbound: false, and every other package is browser only. So the snippets and commands below are a worked example named my-agent, an agent that calls a customer about an appointment. Everything in them is authorable as shown; there is just no package in the repository to run them against. On this page:

Declare the direction

agent.yaml
Without outbound: true, this target has no outbound capability at all: no route reads a destination number for it.

Every key a channel takes

A telephony channel writes both directions, whichever one it uses.
realtime_audio | telephony
required
telephony is a phone call. realtime_audio is the browser channel, and it takes none of the keys below.
true | false
required
Whether this agent answers calls. false on an agent that only dials.
true | false
required
Whether this agent places calls. It is also what a warm transfer needs, since a warm transfer dials its destination.
list of control names
What the route has to support: cold_transfer, warm_transfer, dtmf_send, dtmf_receive, hold, hangup, voicemail_detection, ivr_navigation. A route that cannot do one is refused when you validate.
hangup | leave_message
What the agent does when it reaches a voicemail box. It needs outbound: true and a route that detects voicemail, which today is LiveKit sip.

One route per target

Dialling out is the carrier’s job, so each target needs a connection that can reach your carrier account. An agent on both targets rides two different mechanisms, so it needs two connection files holding the same three names:
Same three names, different transport:. from_number is the caller identity the person you dial sees. Neither target says anything else about the route.

Every key a connection takes

sip | connector | cloud-websocket | daily-sip
required
The mechanism that carries the call. Every one of the four can dial out. The target’s provider decides which of them it accepts.
twilio | telnyx | plivo
required
The carrier account that places the call. Telnyx and Plivo dial out through the LiveKit sip route only.
route key to environment variable name
Which variable holds each of the route’s account values. Dialling out always needs from_number, plus either account_sid and auth_token or the SIP trio, depending on the route. Which environment keys a route accepts has the set per route. The names on the right are yours, and every one is UPPER_SNAKE.
See connections/<name>.yaml for the whole file.

Place a call

A phone call reaches an agent that is deployed, so placing one is the deployed platform’s job, not a local command. Compile my-agent, deploy it to the target’s platform, and use the Place an outbound call section of the generated build/<target>/README.md. It prints the exact request for your route, with the trunk, project, and agent names already filled in:
  • Pipecat cloud-websocket places the call with one request to Twilio’s own API, carrying markup that names the deployed agent.
  • Pipecat daily-sip places it with one request to the Pipecat Cloud dial-out endpoint, through the trunk your carrier setup created.
  • LiveKit sip and LiveKit connector place it by dispatching this agent to a new room with job metadata, and the worker dials with the carrier’s trunk settings inline.
Every one of the four surviving telephony routes can dial out. What differs is the request shape, which is why the README is the one place to copy it from: it already carries your own values.

Give the call its values

An outbound call usually knows things before it starts: who is being called, about what. Declare them as call_start variables:
agent.yaml
Locally, --var supplies them:
In production the same values ride the target’s own dispatch payload, as one flat JSON object. Each generated build/<target>/README.md prints the exact spelling for its platform. That is the whole relationship: --var is the local stand in for the dispatch payload. Same names, same types, same variables. Values are checked against the declared type, and an undeclared name is refused rather than quietly ignored.

Every key a variable takes

type expression
required
What the value is, written as one line: a built-in such as string, a Literal[...] set, a list[...], or a shape you declared. The type grammar has all of them.
call_start | conversation | a system call fact
Where the value comes from. call_start is the dispatch payload, which --var stands in for. conversation is a value the model records mid-call. The eight system facts are listed below. Left out, a task’s assign: fills it.
a value of the declared type
What the value is before anything supplies one. Required on a call_start variable when the channel is also inbound, because an inbound call carries no dispatch payload.
step name
The step that has to hear the caller agree before anything acts on this value. Until then it renders in that step’s prompt and nowhere else. See confirm:.
string
What the value is, for a reader and for the model. Required on a source: conversation variable, because the model reads it to know what to record.

Values the route supplies

Some variables are filled in by the runtime, not by you:
System sources include to_number, from_number, direction, call_id, stream_id, session_id, carrier, and connection. Seeding one with --var is refused, because the runtime owns it. Which routes supply them, and in which direction, differs per fact. Both LiveKit routes supply every system source, on both directions. The two Pipecat Twilio routes supply a smaller set: pipecat cloud-websocket fills to_number on an outbound call, which is exactly the dialed_number example above, while pipecat daily-sip supplies no to_number at all. A route that does not grant a fact refuses the declaration at validation, rather than leaving an empty string at call time. The full grid in the pre-fetch reference has every fact, route and direction. Declaring the fact as a prefetch: entry instead keeps one package compiling on every route. Where the route supplies nothing, the entry is skipped and the variable keeps its default, rather than the package being refused. Where it works covers that shape, under “The number an outbound call carries”, and what a cloud-websocket call has to carry for it to arrive.

Where to go next

Inbound calls

Take a real call, once the route is deployed.

Get your Twilio details

Which console value fills which name.

Variables reference

Types, sources, and where each value can be used.