AI agents
Voice and SMS agents that answer, book and call back with OpenAI.
AI receptionist
An AI agent answers your calls, or takes over when your team does not pick up: it qualifies the caller, books a slot in cal.com and emails you a summary.
How it works
- Your team's phones can ring first (optional); otherwise the AI answers right away.
- The AI assistant asks what the caller needs and answers their questions.
- It checks your cal.com calendar and books a slot when the caller agrees, or transfers to a human on request.
- After the call, your team gets an email summary that says whether an appointment was booked.
- Callers on a mobile phone also get a text recap.
Starts on: inbound-call
Actions used: dial@v2, hangup@v1, voicebot@v1, play@v3, javascript@v1, openai@v3, email@v1, sms@v1
Guide: Multi-Channel AI Agents
# AI receptionist: an AI agent answers your calls, qualifies the caller and
# books a slot in cal.com. Your team's phones can ring first (optional): the
# AI then only takes the calls nobody picks up.
# After the call, it emails a summary to your team and texts mobile callers.
#
# What to edit: $team (leave it empty to let the AI answer right away),
# $cal, $openai, the agent prompt in $messages, $emails.
# Needs: an OpenAI API key and a cal.com account (API key, event type).
# Build Mode: calls and SMS only to and from your Testing Numbers.
# Guide: https://docs.callr.com/docs/multi-channel-ai-agents
description: "AI receptionist"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Olivia:MAI-Voice-2
variables:
$dial: null
$bot: null
$transfer: null
$aiResult: null
$short: null
$long: null
$sms: null
$booked: false
$bookedDate: null
# MODIFIABLE VARIABLES BELOW
# Phones to ring first, all at once. Empty: the AI answers right away.
# Example: - number: "+15550100142"
# timeout: 20
# Keep the timeout below the time your phones take to reach their own
# voicemail (about 20 s), or that voicemail takes the call instead of the AI.
$team: []
# cal.com API key, username, event type and time zone
$cal:
auth: <cal.com-auth-token>
username: <cal.com-username>
eventTypeSlug: <cal.com-event-type-slug>
eventTypeId: <cal.com-event-type-id>
timeZone: Europe/Paris
# OpenAI API key and model used by the agent and the summary
$openai:
auth: <openai-auth-token>
model: gpt-5.4
# voicebot prompt
$messages:
- role: developer
content: >
You are the intelligent answering machine for the TOP REAL ESTATE agency,
located at 128 rue La Boétie, 75008, Paris.
The agency's hours are:
- Monday to Friday from 9am to 5pm
- Saturday from 10am to 4pm
- excluding public holidays
The agency manager is Jean-Michel Mal-Logé. The staff are:
- Sarah Pelle
- Sarah Croche
Your goal is to qualify the call. For example, the caller might be calling to:
- Sell a property
- Buy a property
- Follow up on an ongoing file
- Schedule an appointment
- Inquire about the agency's hours
If they call regarding a property or to schedule an appointment, ask questions to
qualify their request and the property: location, area, desired price, etc.
You must qualify their request as precisely as possible.
If you are asked to call back, say that you will forward the information to the agency.
If an appointment is requested, you can call the function "get_calendar_slot" to check
availability. When listing available times to the caller, provide a maximum of 3 options.
The booking is arranged over the phone with you. Once the caller confirms a slot,
book it with "book_slot" and tell them that they will receive a confirmation.
- Be friendly, with a light touch of humour.
- Ensure that all your responses are short and simple. Use everyday language.
- This is a spoken conversation. Do not reply in markdown.
- Do not use emojis (due to voice synthesis).
You have the following functions (tools) available:
- dial: to transfer the call to a human. phone_number = '+15550100120'. You can use it at the caller's request.
- hangup: when the conversation is over, you can hang up.
- get_calendar_slot: checks availability in the calendar.
- book_slot: books the chosen slot once the caller has confirmed it.
If the caller says goodbye, or if you think the conversation is over, use the hangup function.
- role: developer
content: >
Today is ${{ now.toISOString() }}.
Caller number: ${{ call.fromNumber }}.
# all messages with role "assistant" will be spoken by the bot at the start of the call
- role: assistant
content: >
Hello, I am Maria, the virtual assistant of TOP REAL ESTATE agency.
How can I help you?
# to extract information at the end of the call
$ai: >
Can you reply with a JSON object containing the following properties:
{
"short": "summary of the conversation in a few words",
"long": "summary of the conversation in a few sentences",
"booked": bool (true/false if an appointment is booked),
"booked_date": "ISO 8601 if an appointment is booked",
"sms": "an SMS for the caller, with the appointment date and time if one is booked"
}
# receive the call summary
$emails:
- [email protected]
branches:
inbound-call:
actions:
# ring your team first, if any
- if: $team?.length
then:
- action: dial@v2
params:
parallel: true
targets: ${{ $team }}
result: $dial
# someone picked up and the call is over
- if: $dial?.ok
then:
- action: hangup@v1
# nobody picked up: the AI agent takes the call
- action: voicebot@v1
params:
pleaseWait:
# if the bot takes more than 2s (configurable) to respond,
# the following phrases will be spoken
enabled: true
delay: 2
sentences:
- please hold on
- one moment please
- okay, one moment please
initialMessages: ${{ $messages }}
engine:
name: OpenAI
model: ${{ $openai.model }}
auth: ${{ $openai.auth }}
params:
maxCompletionTokens: 0 # default
temperature: 0.2 # default
# url: https://openai.compatible.endpoint
# the tools defined here are HTTP endpoints that will be called directly by the bot
tools:
- name: get_calendar_slot
url: https://api.cal.com/v2/slots?username=${{ $cal.username }}&eventTypeSlug=${{ $cal.eventTypeSlug }}&timeZone=${{ $cal.timeZone }}
description: Returns a list of available slots
method: GET
headers:
Authorization: Bearer ${{ $cal.auth }}
cal-api-version: 2024-09-04
# the parameters are automatically filled by the bot
params:
- name: start
type: string
description: ISO8601 date-time (start).
- name: end
type: string
description: ISO8601 date-time (end).
- name: book_slot
url: https://api.cal.com/v2/bookings
description: Books a slot for an appointment
headers:
Authorization: Bearer ${{ $cal.auth }}
cal-api-version: 2024-08-13
method: POST
params:
# each parameter must be defined with a JSON SCHEMA
# https://platform.openai.com/docs/guides/function-calling?api-mode=chat#defining-functions
# https://json-schema.org/
- name: attendee
type: object
description: caller information
additionalProperties: false
required:
- phoneNumber
- name
- timeZone
- email
properties:
phoneNumber:
type: string
description: Phone number in E.164
name:
type: string
description: Caller name. Use phone number if empty.
timeZone:
type: string
description: IANA timezone, use ${{ $cal.timeZone }}
email:
type: string
description: use noreply+(phoneNumber)@example.com
- name: start
type: string
description: Slot ISO8601 date-time start
- name: eventTypeId
type: number
description: Use ${{ $cal.eventTypeId }}
result: $bot
# if the bot wishes to call the builtin tool "dial",
# the number to call is in $bot.payload
- if: $bot?.exitTool === 'dial' && $bot?.payload?.match(/^\+/)
then:
- action: dial@v2
params:
targets:
- number: ${{ $bot?.payload }}
timeout: 30
result: $transfer
# nobody answered the transfer
- if: "!$transfer?.ok"
then:
- action: play@v3
params:
tts:
text: Nobody is available right now. We will call you back.
- action: hangup@v1
# if the bot wishes to hang up
- if: $bot?.exitTool === 'hangup'
then:
- action: play@v3
params:
tts:
text: Goodbye, see you soon
- action: hangup@v1
hangup:
actions:
# the conversation history (in OpenAI format) is in $bot.messages
# (empty when your team took the call)
- if: $bot?.messages?.length
then:
# we add a message to extract information using $ai
- action: javascript@v1
params:
code: |
const $messages = $bot.messages
$messages.push({
role: 'developer',
content: $ai
})
return {
$messages
}
# we call OpenAI in one-shot by passing the entire history
# plus the $ai prompt added above
- action: openai@v3
params:
secret: ${{ $openai.auth }}
endpoint: chat/completions
timeout: 60
properties:
model: ${{ $openai.model }}
messages: ${{ $messages }}
response_format:
type: json_object
result: $aiResult
# we parse the JSON response, with a fallback if OpenAI failed
- action: javascript@v1
params:
code: |
let res = {}
if ($aiResult?.ok && $aiResult?.choices?.length) {
try {
res = JSON.parse($aiResult?.choices?.[0]?.message?.content || '{}')
} catch (e) {
res = {}
}
}
return {
$short: res.short || 'no summary',
$long: res.long || 'The AI summary is not available. See the call in your Callr logs.',
$sms: res.sms || null,
$booked: res.booked === true,
$bookedDate: res.booked_date || null
}
# we send a summary via email
- action: email@v1
params:
subject: Call handled by the AI assistant from ${{ call.fromNumber ?? '(hidden number)' }} - ${{ $short }}
text: |
Call handled by the AI assistant from ${{ call.fromNumber ?? '(hidden number)' }}
${{ $long }}
${{ $booked ? 'Appointment booked: ' + ($bookedDate || 'see cal.com') : 'No appointment booked.' }}
Regards.
to: ${{ $emails }}
# if the caller is on a mobile phone, also send them a summary
- if: $sms && call.fromType === 'MOBILE'
then:
- action: sms@v1
params:
to:
- ${{ call.fromNumber }}
body: ${{ $sms }}
nature: ALERTINGAI agent that calls your customers
Your app starts the call by API and an AI voice agent talks to your customer, then the outcome is summarised and sent to your webhook and inbox.
How it works
- Your app starts the scenario with the number to call and the customer details.
- If an answering machine picks up, a short message is left and the call ends.
- When the customer answers, an AI voice agent confirms the appointment, or the customer can ask to speak to your team.
- After the call, AI extracts the outcome and a short summary.
- The outcome is posted to your webhook and emailed to you.
Starts on: api-initiated
Actions used: dial@v2, javascript@v1, openai@v3, fetch@v3, email@v1, amd@v3, waitForSilence@v1, play@v3, hangup@v1, voicebot@v1
Guide: Broadcast Calls
# This is an API-initiated template
# To start this scenario, you can click on "test"
# on the right bar, and then "START ACTION".
#
# To see how to start it with the API,
# click "SHOW API CODE".
#
# Outbound AI agent: your app starts a run with the number to call and the
# customer details. When the person answers, an AI voice agent confirms the
# appointment (or qualifies a lead, runs a survey: change the prompt). It can
# transfer to your team on request. After the call, the outcome is extracted
# with AI and posted to your webhook and emailed to you.
#
# What to edit: $callerId, $openai, the agent prompt in $messages, $human,
# $webhookUrl, $emails; pass $target and $context in each API call.
#
# Needs: an OpenAI API key and a caller ID: a Callr number attached to an
# active scenario or an approved caller ID. In Build Mode, calls only reach
# your Testing Numbers. In France, a caller ID is blocked above 250 calls a
# day (use NPV numbers). https://docs.callr.com/docs/caller-ids
#
# If your server filters by IP address, allow the Callr subnets:
# https://docs.callr.com/docs/ip-subnets
#
# Guide: https://docs.callr.com/docs/broadcast-calls
description: "AI agent that calls your customers"
compat:
version: "2025-09-01"
defaults:
language: en-US
voice: en-US-Olivia:MAI-Voice-2
# Every variable can be overridden for each run started by API
variables:
$dial: null
$amd: null
$prompt: null
$bot: null
$transfer: null
$aiMessages: null
$aiResult: null
$summary: null
# completed, transferred, transfer_failed, voicemail, or why the call failed (noanswer, busy...)
$outcome: "unknown"
# MODIFIABLE VARIABLES BELOW
# Number to call, pass it in each API call
$target: "+33639984241"
# Caller ID shown to the customer: a number assigned or approved on your account
$callerId: "+33199001234"
# Customer details given to the agent, pass them in each API call
$context:
name: Emma Martin
appointment: tomorrow at 10 AM with Dr. Bernard
# Your OpenAI API key and the model of the agent
$openai:
auth: <openai-key>
model: gpt-5.4
# Your team, reached when the customer asks for a human (null: no transfer)
$human: "+33199004210"
# Receives the outcome (POST, JSON), e.g. https://api.example.com/callr/outbound
$webhookUrl: null
# Receive the outcome by email (empty: no email)
$emails:
- [email protected]
# The agent prompt: the customer details, the date and the first sentence are added below
$messages:
- role: developer
content: >
You are Olivia, a voice assistant calling on behalf of the dental practice
"Bright Smile", 128 rue La Boétie, 75008 Paris.
You call a customer to confirm their appointment.
Your goal: the customer confirms, cancels, or asks for another slot.
If they want another slot, ask which days and times suit them, and say
the practice will call back to confirm.
Save the answer with set_value: name "outcome", value "confirmed",
"canceled" or "reschedule"; name "preferred_slot" if they gave one.
If you reach the wrong person, apologize and hang up.
If the customer asks for a human, call the dial function.
When the conversation is over, say goodbye and call the hangup function.
- This is a phone call: short sentences, casual language, no emoticons.
- Do not invent details.
# What the AI extracts from the conversation after the call (JSON)
$extractPrompt: >
Read the conversation above and answer in JSON with these keys:
"outcome" (one of "confirmed", "canceled", "reschedule", "no_decision"),
"callback_requested" (true or false),
"summary" (two sentences for the practice staff).
branches:
# Runs when the API starts the scenario
api-initiated:
actions:
# Call the customer. This action waits until the call ends;
# the answered call runs the 'agent' branch.
- action: dial@v2
params:
callerId: ${{ $callerId }}
targets:
- number: ${{ $target }}
timeout: 30
answerOnBranch: agent
result: $dial
# $amd, $bot and $transfer are set on the customer's call ('agent' branch)
# and read here once the call has ended, as in the appointment reminder
# template: check it once in the live debugger.
- action: javascript@v1
params:
code: |
if (!['answered', 'bridged'].includes($dial?.status)) return { $outcome: $dial?.status ?? 'failed' }
if ($amd?.detected == 'machine') return { $outcome: 'voicemail' }
if ($bot?.exitTool === 'dial' && $human) return { $outcome: $transfer?.ok ? 'transferred' : 'transfer_failed' }
return { $outcome: 'completed' }
# Extract the outcome from the conversation with AI
- if: $bot?.messages?.length
then:
- action: javascript@v1
params:
code: |
return {
$aiMessages: [...$bot.messages, { role: 'developer', content: $extractPrompt }]
}
- action: openai@v3
params:
secret: ${{ $openai.auth }}
endpoint: chat/completions
timeout: 30
properties:
model: ${{ $openai.model }}
messages: ${{ $aiMessages }}
response_format:
type: json_object
result: $aiResult
# Parse the JSON answer, with a fallback if OpenAI failed
- action: javascript@v1
params:
code: |
let res = null
if ($aiResult?.ok && $aiResult?.choices?.length) {
try {
res = JSON.parse($aiResult?.choices?.[0]?.message?.content || 'null')
} catch (e) {
res = null
}
}
return { $summary: res }
# Post the outcome to your server
- if: $webhookUrl
action: fetch@v3
params:
url: ${{ $webhookUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"target": ${{ JSON.stringify($target) }},
"context": ${{ JSON.stringify($context) }},
"status": ${{ JSON.stringify($dial?.status ?? null) }},
"outcome": ${{ JSON.stringify($outcome) }},
"summary": ${{ JSON.stringify($summary) }},
"values": ${{ JSON.stringify($bot?.values ?? null) }}
}
# Email the outcome
- if: $emails?.length
action: email@v1
params:
to: ${{ $emails }}
subject: Call to ${{ $context?.name ?? $target }} - ${{ $summary?.outcome ?? $outcome }}
text: |
Call to ${{ $context?.name ?? '' }} (${{ $target }}): ${{ $outcome }}
${{ $summary?.summary ?? 'No AI summary for this call.' }}
Callback requested: ${{ $summary?.callback_requested ? 'yes' : 'no' }}
Values saved by the agent: ${{ JSON.stringify($bot?.values ?? {}) }}
# Runs on the customer's call once answered
agent:
actions:
# Answering machine: leave a short message after the beep
- action: amd@v3
params:
timeout: 4
result: $amd
- if: $amd?.detected == 'machine'
then:
- action: waitForSilence@v1
params:
timeout: 10
- action: play@v3
params:
tts:
text: Hello, this is the Bright Smile dental practice, about your appointment. We will call you back.
- action: hangup@v1
# Add the customer details, the date and the first sentence to the prompt
- action: javascript@v1
params:
code: |
return {
$prompt: [
...$messages,
{ role: 'developer', content: 'Today is ' + new Date(now).toISOString() + '. Customer: ' + JSON.stringify($context) + ($human ? '' : '. No transfer is possible: offer a callback instead.') },
{ role: 'assistant', content: ($context?.name ? 'Hello ' + $context.name : 'Hello') + ', this is Olivia from Bright Smile, about your appointment' + ($context?.appointment ? ' ' + $context.appointment : '') + '. Can you still make it?' }
]
}
- action: voicebot@v1
params:
initialMessages: ${{ $prompt }}
engine:
name: OpenAI
model: ${{ $openai.model }}
auth: ${{ $openai.auth }}
# Short sentences while the AI thinks
pleaseWait:
enabled: true
result: $bot
# The customer asked for a human: always your team, never a number from the AI
- if: $bot?.exitTool === 'dial' && $human
then:
- action: play@v3
params:
tts:
text: Please hold, I am transferring your call.
- action: dial@v2
params:
callerId: ${{ $callerId }}
targets:
- number: ${{ $human }}
timeout: 30
result: $transfer
- if: "!$transfer?.ok"
action: play@v3
params:
tts:
text: Sorry, nobody is available. We will call you back.
# Goodbye, also when a human was asked for but no transfer is set up
- if: $bot?.exitTool === 'hangup' || ($bot?.exitTool === 'dial' && !$human)
action: play@v3
params:
tts:
text: Goodbye, have a nice day.
- action: hangup@v1Your voice AI over SIP
Bridges your Callr number over SIP to your Vapi or ElevenLabs agent (or OpenAI Realtime, with your own server accepting the calls), with recording and a human backup.
How it works
- Someone calls your Callr number and hears that the call may be recorded.
- Callr connects the call over SIP to your voice AI agent, which handles the whole conversation.
- If the agent does not pick up within 20 seconds, your human backup number rings.
- If nobody answers, the caller hears a short apology.
- When the call ends, the recording and transcript can be sent to your webhook.
Starts on: inbound-call
Actions used: play@v3, startRecording@v2, fetch@v3, dial@v2, answer@v1, hangup@v1
Guide: Connect to Vapi
# Bridges every inbound call over SIP/TLS to your own voice AI agent
# (Vapi, ElevenLabs or OpenAI Realtime) and records it on the Callr side.
# If the agent does not pick up within 20 seconds, a human number rings.
# If nobody answers, the caller hears a short apology.
#
# What to edit: $agentSipUri, $humanFallback, $apology and $recordingNotice
# (set defaults.language and voice to fr-FR / fr-FR-Soleil:MAI-Voice-2 for French).
# Needs: a number assigned to this scenario, and on the AI platform side:
# - Vapi: an assistant with SIP enabled (it gives the SIP URI). Nothing else.
# - ElevenLabs: your Callr number imported as a SIP trunk number (Phone
# Numbers > Import number > From SIP Trunk, media encryption required)
# and an agent assigned to it. https://docs.callr.com/docs/connect-to-elevenlabs
# - OpenAI Realtime: OpenAI never answers a SIP call by itself. A server of
# yours must receive the project webhook (realtime.call.incoming, or
# live.transport.incoming with the Live API) and accept each call with
# POST /v1/realtime/calls/{call_id}/accept (or /v1/live/sessions/{session_id}/accept),
# which also sets the model and instructions. Without it the call never connects.
# https://developers.openai.com/api/docs/guides/realtime-sip
# Build Mode: calls and SMS go only to your Testing Numbers.
# Webhook calls come from these IP ranges: https://docs.callr.com/docs/ip-subnets
# Guides: https://docs.callr.com/docs/connect-to-vapi
# https://docs.callr.com/docs/connect-to-openai
description: "Your voice AI over SIP"
compat:
version: "2025-09-01"
defaults:
# The call is answered so that it can be recorded.
# Alternative: no recording and a transparent transfer (the caller is billed
# only if the agent or the human picks up): set autoAnswer: false and $record: false
# (the call is then answered only before the apology).
language: en-US
voice: en-US-Harper:MAI-Voice-2
variables:
$dial: null
# MODIFIABLE VARIABLES BELOW
$agentSipUri: sip:<assistant-id>@sip.vapi.ai;transport=tls # your Vapi assistant
# ElevenLabs: the user part is your Callr number, exactly as imported in ElevenLabs
# $agentSipUri: sip:[email protected]:5061;transport=tls
# OpenAI Realtime: needs your server to accept each call (see the header)
# $agentSipUri: sip:<project-id>@sip.api.openai.com;transport=tls
$humanFallback: "+33199004242" # rings if the agent does not answer
$record: true # record the call on the Callr side (false to disable)
$recordingNotice: This call may be recorded. # said before recording, null if your agent says it
$apology: Sorry, we are unable to take your call right now. Please call back later. # said when neither the agent nor the human answers
$webhookUrl: null # receives the recording and transcript, e.g., https://api.example.com/callr
branches:
inbound-call:
actions:
# Announce the recording before it starts, where the law requires it
- if: $record && $recordingNotice
action: play@v3
params:
tts:
text: ${{ $recordingNotice }}
- if: $record
action: startRecording@v2
events:
# Executed when the recording and transcript are ready
ready:
- if: $webhookUrl
action: fetch@v3
params:
method: POST
url: ${{ $webhookUrl }}
headers:
Content-Type: application/json
body: |
{
"call": ${{ JSON.stringify(call) }},
"recordingURL": ${{ JSON.stringify(recording?.url) }},
"duration": ${{ JSON.stringify(recording?.duration) }},
"transcript": ${{ JSON.stringify(chat_text) }}
}
# The agent first, then the human (one after the other)
- action: dial@v2
params:
parallel: false
targets:
- number: ${{ $agentSipUri }}
timeout: 20
- number: ${{ $humanFallback }}
timeout: 60
result: $dial
# Neither the agent nor the human answered
- if: "!$dial?.ok"
then:
- action: answer@v1
- action: play@v3
params:
tts:
text: ${{ $apology }}
- action: hangup@v1AI SMS agent
An AI answers every text from your business facts, remembers each conversation for an hour, respects STOP and emails the thread to your team once when a person is needed.
How it works
- Someone texts your number.
- If the text starts with STOP, they get an opt-out confirmation and the AI is never involved.
- Otherwise the AI reads the conversation of the last hour and writes a short answer in your brand's voice.
- The answer is texted back from the same number, so the customer can simply reply.
- If the customer asks for a person or the AI cannot help, your team receives the whole thread by email, once per conversation.
Starts on: inbound-sms
Actions used: javascript@v1, sms@v1, fetch@v3, memcache@v1, openai@v3, email@v1, log@v1
Guide: Multi-Channel AI Agents
# AI SMS agent: an OpenAI model answers every text sent to your number,
# in your brand's voice, from the facts you put in $system.
# It remembers the conversation with each sender for one hour.
# STOP always gets an opt-out confirmation and never reaches the AI.
# When the customer asks for a person (or the AI cannot help), the whole
# thread is emailed to your team, once per conversation.
#
# What to edit: $openai (API key, model), $system (your business facts
# and tone), $escalationEmail, and optionally $maxMessages and $webhookUrl.
# Store opt-outs on your server ($webhookUrl, "event": "optout"),
# and check that list before you send any campaign.
#
# Needs: an SMS-capable number assigned to this scenario, an OpenAI API key.
# In Build Mode, SMS are only sent to your Testing Numbers:
# text the number from one of them to try it.
#
# If your server filters by IP address, allow the Callr subnets:
# https://docs.callr.com/docs/ip-subnets
#
# Guide: https://docs.callr.com/docs/multi-channel-ai-agents
description: "AI SMS agent"
compat:
version: "2025-09-01"
defaults:
# SMS only: no speech in this scenario
language: en-US
variables:
$text: ""
$cache: null
$messages: []
$aiResult: null
$reply: ""
$escalate: false
$escalated: false
$notify: false
$reason: ""
$history: []
$thread: ""
$state: null
# MODIFIABLE VARIABLES BELOW
# OpenAI API key and model
$openai:
auth: <openai-auth-token>
model: gpt-5.4-mini
# what the agent knows and how it writes
$system: >
You are the SMS assistant of Example Bikes, a bike shop in Paris.
Opening hours: Monday to Saturday, 10 am to 7 pm.
Services: repairs in 48 hours, bike rental from 15 euros a day.
Reply in the customer's language, in plain text, in less than 300 characters.
Be friendly and short. Never invent prices, stock or dates.
If the customer asks for a person, is unhappy, or you cannot answer,
say that a team member will text them back soon and set escalate to true.
Answer only with this JSON: {"reply": "your SMS", "escalate": true or false}
# receives the thread when a person is needed
$escalationEmail: [email protected]
# messages kept in memory per sender (one hour at most)
$maxMessages: 20
# receives opt-outs as JSON (POST), e.g. https://api.example.com/callr/optout
$webhookUrl: null
branches:
inbound-sms:
actions:
- action: javascript@v1
params:
code: $text = (sms.body?.text ?? '').trim()
# STOP (first word): transactional confirmation only, the AI never answers it
- if: /^stop\b/i.test($text)
then:
- action: sms@v1
params:
to:
- ${{ sms.from }}
from: ${{ sms.to }}
body: You are unsubscribed and will not receive any more messages from us.
nature: ALERTING
- if: $webhookUrl
action: fetch@v3
params:
url: ${{ $webhookUrl }}
method: POST
headers:
Content-Type: application/json
body: |
{
"event": "optout",
"from": ${{ JSON.stringify(sms.from) }},
"to": ${{ JSON.stringify(sms.to) }}
}
- goto:
label: done
# The conversation with this sender (memcache keeps it one hour
# after the last message)
- action: memcache@v1
params:
command: get
key: sms-agent-history-${{ sms.from }}
result: $cache
- action: javascript@v1
params:
code: |
const saved = $cache?.ok ? $cache.value : null
$history = Array.isArray(saved?.history) ? saved.history : []
$escalated = saved?.escalated === true
$history.push({ role: 'user', content: $text })
$messages = [{ role: 'developer', content: $system }, ...$history]
- action: openai@v3
params:
secret: ${{ $openai.auth }}
endpoint: chat/completions
timeout: 30
properties:
model: ${{ $openai.model }}
messages: ${{ $messages }}
response_format:
type: json_object
result: $aiResult
# Read the JSON answer. If OpenAI fails or the answer cannot be read,
# send a holding reply and hand over to a person.
- action: javascript@v1
params:
code: |
let res = {}
try {
res = JSON.parse($aiResult?.choices?.[0]?.message?.content || '{}')
} catch (e) {
res = {}
}
$reply = String(res.reply || 'Thanks for your message! A team member will get back to you shortly.').slice(0, 1000)
$escalate = res.escalate === true || !res.reply
$reason = !$aiResult?.ok ? 'The AI could not answer (' + String($aiResult?.error ?? 'no answer') + ').'
: !res.reply ? 'The AI answer could not be read.'
: 'The customer needs a person.'
// one email per conversation: not again if the team already has it
$notify = $escalate && !$escalated
$history.push({ role: 'assistant', content: $reply })
$history = $history.slice(-$maxMessages)
$state = { history: $history, escalated: $escalated || $escalate }
$thread = $history.map(m => (m.role === 'user' ? 'Customer: ' : 'Agent: ') + m.content).join('\n')
- action: sms@v1
params:
to:
- ${{ sms.from }}
from: ${{ sms.to }}
body: ${{ $reply }}
nature: ALERTING
- action: memcache@v1
params:
command: set
key: sms-agent-history-${{ sms.from }}
value: ${{ $state }}
ttl: 3600
# A person is needed: email the whole thread (first time only)
- if: $notify
action: email@v1
params:
to:
- ${{ $escalationEmail }}
subject: SMS conversation needs a person (${{ sms.from }})
text: |
${{ $reason }}
Text ${{ sms.from }} back from ${{ sms.to }}.
${{ $thread }}
- label: done
action: log@v1
params:
message: SMS from ${{ sms.from }} handled, escalate=${{ $escalate }}Updated about 1 hour ago
