I'm new to developing IVR with twilio studio so I started with the basic template and even that's not working.
This is the log:
LOG
Split Based On...
DETAIL
Input evaluated to 'Sales.' from '{{widgets.gather_input.SpeechResult}}'
Transitioning to 'say_play_1' because 'Sales.' did not match any expression
The split is set to "Equal to" sales which then connects the call to a number. It's obviously recognizing the correct speech input but still not working. Any ideas?
{
"description": "IVR",
"states": [
{
"name": "Trigger",
"type": "trigger",
"transitions": [
{
"event": "incomingMessage"
},
{
"next": "gather_input",
"event": "incomingCall"
},
{
"event": "incomingRequest"
}
],
"properties": {
"offset": {
"x": 250,
"y": 50
}
}
},
{
"name": "gather_input",
"type": "gather-input-on-call",
"transitions": [
{
"next": "split_key_press",
"event": "keypress"
},
{
"next": "split_speech_result",
"event": "speech"
},
{
"event": "timeout"
}
],
"properties": {
"voice": "alice",
"speech_timeout": "auto",
"offset": {
"x": 290,
"y": 250
},
"loop": 1,
"hints": "support,sales",
"finish_on_key": "",
"say": "Hello, how can we direct your call? Press 1 for sales, or say sales. To reach support, press 2 or say support.",
"language": "en",
"stop_gather": false,
"gather_language": "en-US",
"profanity_filter": "false",
"timeout": 5
}
},
{
"name": "split_key_press",
"type": "split-based-on",
"transitions": [
{
"event": "noMatch"
},
{
"next": "connect_call_to_sales",
"event": "match",
"conditions": [
{
"friendly_name": "1",
"arguments": [
"{{widgets.gather_input.Digits}}"
],
"type": "equal_to",
"value": "1"
}
]
},
{
"next": "connect_call_to_support",
"event": "match",
"conditions": [
{
"friendly_name": "2",
"arguments": [
"{{widgets.gather_input.Digits}}"
],
"type": "equal_to",
"value": "2"
}
]
}
],
"properties": {
"input": "{{widgets.gather_input.Digits}}",
"offset": {
"x": 100,
"y": 510
}
}
},
{
"name": "split_speech_result",
"type": "split-based-on",
"transitions": [
{
"next": "say_play_1",
"event": "noMatch"
},
{
"next": "connect_call_to_sales",
"event": "match",
"conditions": [
{
"friendly_name": "sales",
"arguments": [
"{{widgets.gather_input.SpeechResult}}"
],
"type": "equal_to",
"value": "sales"
}
]
},
{
"next": "connect_call_to_support",
"event": "match",
"conditions": [
{
"friendly_name": "support",
"arguments": [
"{{widgets.gather_input.SpeechResult}}"
],
"type": "equal_to",
"value": "support"
}
]
}
],
"properties": {
"input": "{{widgets.gather_input.SpeechResult}}",
"offset": {
"x": 510,
"y": 510
}
}
},
{
"name": "connect_call_to_sales",
"type": "connect-call-to",
"transitions": [
{
"event": "callCompleted"
}
],
"properties": {
"offset": {
"x": 100,
"y": 750
},
"caller_id": "{{contact.channel.address}}",
"noun": "number",
"to": "12222222",
"timeout": 30
}
},
{
"name": "connect_call_to_support",
"type": "connect-call-to",
"transitions": [
{
"event": "callCompleted"
}
],
"properties": {
"offset": {
"x": 520,
"y": 750
},
"caller_id": "{{contact.channel.address}}",
"noun": "number",
"to": "12222222",
"timeout": 30
}
},
{
"name": "say_play_1",
"type": "say-play",
"transitions": [
{
"next": "gather_input",
"event": "audioComplete"
}
],
"properties": {
"offset": {
"x": 710,
"y": 200
},
"loop": 1,
"say": "not valid choice."
}
}
],
"initial_state": "Trigger",
"flags": {
"allow_concurrent_calls": true
}
}
Twilio developer evangelist here.
That is weird behaviour, mainly because the log says "evaluated to 'Sales.'". Split widgets conditions are not case-sensitive and should trim leading and following white-space. For some reason this appears to have a capital "S" and a full-stop.
I would suggest a couple of things. Firstly, raise a ticket with Twilio support to look into why the condition didn't match correctly.
Then, try some of the other conditions. When I generate a new IVR template in Studio, the conditions use "Matches any of" instead of "Equal to". You might also try "Contains".
So, experiment with the ways you can match the operators, but get support involved to drill down into why it didn't work in the first place.
The period is needed after the word for some reason...I just put both, for example:
"1, 1., Uno, Uno., Una, Una., Uno, Uno., Español, Español., Español, Español., Español, Español."
Related
Hoping someone can point me in the right direction. I'd like to be able to do this via Twilio Studio. If not, I can learn TwiML. That's about as far as my brain will stretch.
I've made a simple flow in Twilio Studio that enables the caller to record a voicemail. I would like to add an option for the current caller to be able to play the previous caller's recorded voicemail. I think I need to use a Say/Play widget for this. What do I need to use for the "URL of audio file" so that the previous recorded voicemail is played? I assume this URL will change every time that a caller leaves a voicemail, so it'll need to auto-update. Can I use "RecordingURL" somehow? Is there a solution using TwiML? Any help appreciated
Thanks!
Without a way to keep state between the call, this is not possible. The best way would be to have some type of DB you can store the recording SID and reference it in a future flow. You can use a tool like Twilio Sync to do this or Airtable, but it does require code.
I can't think of a way to do this without involving some coding.
Alternate ways are to list the recording records and pull the most recent one off the list but not ideal since you don't know when the last recording occurred.
Another approach is to modify the webhook associated with your Twilio phone number for that Studio Flow to pass in the last Recording SID into your flow and you use that SID to dynamically construct the recording URL to playback, as shown in the JSON flow below (which you can import when creating a new Studio Flow).
Don't forget to set the recordingStatuscallback to the above Twilio Function which updates the phone number webhook to pass in the recording SID. You set your Recording Status Callback of your Record Voicemail Widget to point to your unique Function domain and function path (currently set to: https://magnolia-kitty-3234.twil.io/phUpdate).
Feel free to improve on the below and share.
Twilio Function Code
exports.handler = function(context, event, callback) {
let client = context.getTwilioClient();
const telephoneNumAccountSid = "PN..."; \\ set this to your Phone Number SID
const accountSid = event.AccountSid;
const studioFlowSid = "FW..."; \\ set this to your Studio Flow SID
const webhookUrl = `https://webhooks.twilio.com/v1/Accounts/${accountSid}/Flows/${studioFlowSid}`;
const recordingSid = event.RecordingSid;
client
.incomingPhoneNumbers(telephoneNumAccountSid)
.update({ voiceUrl: `${webhookUrl}?recording=${recordingSid}`, voiceFallbackUrl: `${webhookUrl}?recording=${recordingSid}` })
.then (result => {
console.log(result.voiceUrl);
callback(null, "success");
})
.catch(err => {
console.log(err);
callback("error");
});
};
Studio Flow JSON
{
"description": "A New Flow",
"states": [
{
"name": "Trigger",
"type": "trigger",
"transitions": [
{
"event": "incomingMessage"
},
{
"next": "set_variables_1",
"event": "incomingCall"
},
{
"event": "incomingRequest"
}
],
"properties": {
"offset": {
"x": 0,
"y": 0
}
}
},
{
"name": "split_1",
"type": "split-based-on",
"transitions": [
{
"next": "say_play_2",
"event": "noMatch"
},
{
"next": "gather_1",
"event": "match",
"conditions": [
{
"friendly_name": "{{trigger.call.recording}}",
"arguments": [
"{{trigger.call.recording}}"
],
"type": "is_not_blank",
"value": "Is Not Blank"
}
]
}
],
"properties": {
"input": "{{trigger.call.recording}}",
"offset": {
"x": 110,
"y": 350
}
}
},
{
"name": "say_play_1",
"type": "say-play",
"transitions": [
{
"event": "audioComplete"
}
],
"properties": {
"play": "https://api.twilio.com/2010-04-01/Accounts/{{flow.variables.accountSid}}/Recordings/{{flow.variables.recording}}.mp3",
"offset": {
"x": 450,
"y": 1110
},
"loop": 1
}
},
{
"name": "set_variables_1",
"type": "set-variables",
"transitions": [
{
"next": "split_1",
"event": "next"
}
],
"properties": {
"variables": [
{
"value": "{{trigger.call.recording}}",
"key": "recording"
},
{
"value": "{{trigger.call.AccountSid}}",
"key": "accountSid"
}
],
"offset": {
"x": 60,
"y": 170
}
}
},
{
"name": "say_play_2",
"type": "say-play",
"transitions": [
{
"next": "record_voicemail_1",
"event": "audioComplete"
}
],
"properties": {
"voice": "Polly.Joanna-Neural",
"offset": {
"x": -120,
"y": 650
},
"loop": 1,
"say": "Please leave a message at the beep!",
"language": "en-US"
}
},
{
"name": "record_voicemail_1",
"type": "record-voicemail",
"transitions": [
{
"event": "recordingComplete"
},
{
"event": "noAudio"
},
{
"event": "hangup"
}
],
"properties": {
"transcribe": false,
"offset": {
"x": -120,
"y": 860
},
"trim": "trim-silence",
"play_beep": "true",
"recording_status_callback_url": "https://magnolia-kitty-3234.twil.io/phUpdate",
"timeout": 5,
"max_length": 3600
}
},
{
"name": "gather_1",
"type": "gather-input-on-call",
"transitions": [
{
"next": "split_2",
"event": "keypress"
},
{
"event": "speech"
},
{
"event": "timeout"
}
],
"properties": {
"number_of_digits": 1,
"speech_timeout": "auto",
"offset": {
"x": 300,
"y": 650
},
"loop": 1,
"finish_on_key": "#",
"say": "There is a previous recording, press 1 if you want to listen to it or 2 if you want to leave a new voicemail.",
"stop_gather": true,
"gather_language": "en",
"profanity_filter": "true",
"timeout": 5
}
},
{
"name": "split_2",
"type": "split-based-on",
"transitions": [
{
"event": "noMatch"
},
{
"next": "say_play_1",
"event": "match",
"conditions": [
{
"friendly_name": "If value equal_to 1",
"arguments": [
"{{widgets.gather_1.Digits}}"
],
"type": "equal_to",
"value": "1"
}
]
},
{
"next": "say_play_2",
"event": "match",
"conditions": [
{
"friendly_name": "If value equal_to 2",
"arguments": [
"{{widgets.gather_1.Digits}}"
],
"type": "equal_to",
"value": "2"
}
]
}
],
"properties": {
"input": "{{widgets.gather_1.Digits}}",
"offset": {
"x": 280,
"y": 870
}
}
}
],
"initial_state": "Trigger",
"flags": {
"allow_concurrent_calls": true
}
}
Alan
I'm currently working on a pay-by-phone system with this flow:
<Gather> customer's account number with us
<Gather> amount to pay
<Pay> - with the previous 2 <Gather>s passed as variables to Stripe
The issue I am having is collecting the amount. Is there a way to convert the amount from a single string to a decimal string?
eg. 12300 becomes 123.00
All suggestions welcome as the current working theory is to add a third <Gather> just for the cents but this feels cumbersome from a UX/UI perspective.
I edited this answer, since I was successful in accomplishing this.
I did it with STUDIO and FUNCTIONS.
Prompt the caller to enter the amount using star key () as a decimal. For example to enter $5.43 they should enter 543 and then press #.
then I took the gathered digits and sent them out as a parameter in the FUNCTION widget by inserting "amount" as the KEY and {{widgets.gather_1.digits}} as the VALUE of the parameter.
I wrote the following function:
exports.handler = function(context, event, callback) {
const rawamount = event.amount;
const decimalamount = rawamount.replace("*",".");
const response = decimalamount;
callback(null, response);
};
then in the <pay> widget I inserted {{widget.function_1.body}} in the AMOUNT field.
Your payment should process with dollars and cents.
Let me know if this worked. If you need screenshots of my studio flow let me know.
ADDED:
Here is a sample flow if someone wants to use as a template (someone requested it)
Open a blank Studio flow.
In the trigger widget under "config" tab click on "SHOW FLOW JSON" delete all previous code and paste code below inside and save.
Your flow should generate.
Then create function. Code and instructions below after JSON.
PLEASE NOTE: though a function may pop up in the flow, you won't be able to access it, and will have to create it under your account.
{
"description": "A New Flow",
"states": [
{
"name": "Trigger",
"type": "trigger",
"transitions": [
{
"event": "incomingMessage"
},
{
"next": "gather_1",
"event": "incomingCall"
},
{
"event": "incomingRequest"
}
],
"properties": {
"offset": {
"x": 0,
"y": 0
}
}
},
{
"name": "gather_1",
"type": "gather-input-on-call",
"transitions": [
{
"next": "function_1",
"event": "keypress"
},
{
"event": "speech"
},
{
"event": "timeout"
}
],
"properties": {
"voice": "man",
"speech_timeout": "auto",
"offset": {
"x": 30,
"y": 270
},
"loop": 1,
"finish_on_key": "#",
"say": "Please enter an amount, then press pound. To enter with cents use the star key. for example to enter $6.25 press six star two five.",
"language": "en-US",
"stop_gather": true,
"gather_language": "en",
"profanity_filter": "true",
"timeout": 5
}
},
{
"name": "function_1",
"type": "run-function",
"transitions": [
{
"next": "say_play_2",
"event": "success"
},
{
"event": "fail"
}
],
"properties": {
"offset": {
"x": -60,
"y": 530
},
"parameters": [
{
"value": "{{widgets.gather_1.Digits}}",
"key": "amount"
}
],
"url": "https://charcoal-sloth-2579.twil.io/insert-decimal"
}
},
{
"name": "say_play_2",
"type": "say-play",
"transitions": [
{
"next": "pay_1",
"event": "audioComplete"
}
],
"properties": {
"voice": "man",
"offset": {
"x": -59,
"y": 839
},
"loop": 1,
"say": "You entered ${{widgets.function_1.body}}",
"language": "en-US"
}
},
{
"name": "pay_1",
"type": "capture-payments",
"transitions": [
{
"next": "say_play_3",
"event": "success"
},
{
"next": "say_play_4",
"event": "maxFailedAttempts"
},
{
"next": "say_play_4",
"event": "providerError"
},
{
"next": "say_play_4",
"event": "payInterrupted"
},
{
"next": "say_play_4",
"event": "hangup"
},
{
"next": "say_play_4",
"event": "validationError"
}
],
"properties": {
"security_code": true,
"offset": {
"x": -70,
"y": 1140
},
"max_attempts": 2,
"payment_connector": "Stripe_Connector",
"payment_amount": "{{widgets.function_1.body}}",
"currency": "usd",
"language": "en-US",
"postal_code": "false",
"payment_token_type": "one-time",
"timeout": 5,
"valid_card_types": [
"visa",
"master-card",
"amex",
"discover"
]
}
},
{
"name": "say_play_3",
"type": "say-play",
"transitions": [
{
"event": "audioComplete"
}
],
"properties": {
"voice": "man",
"offset": {
"x": -185,
"y": 1433
},
"loop": 1,
"say": "Your payment ${{widgets.function_1.body}}was successful, Thank you.",
"language": "en-US"
}
},
{
"name": "say_play_4",
"type": "say-play",
"transitions": [
{
"event": "audioComplete"
}
],
"properties": {
"voice": "man",
"offset": {
"x": 190,
"y": 1456
},
"loop": 1,
"say": "There was an error with your payment. Goodbye!",
"language": "en-US"
}
}
],
"initial_state": "Trigger",
"flags": {
"allow_concurrent_calls": true
}
}
Create a function with this code:
exports.handler = function(context, event, callback) {
const amount = event.amount;
const convert = amount.replace('*', '.');
callback(null, convert);
};
Make sure in the flow you check that the function widget has the right function selected and that you insert the following parameter KEY:amount VALUE: {{widgets.YOUR_GATHER_WIDGET NAME.body}}
I'm trying to add padding to my adaptive card view so that it's contents are inset from the edge of the card. I'd like to not adjust the padding of any of the internal card elements. I'm trying to use the following host config, which parses without error but seems to have no effect on the card.
Host config JSON:
{
"spacing": {
"small": 3,
"default": 8,
"medium": 20,
"large": 30,
"extraLarge": 40,
"padding": 100
},
"adaptiveCard": {
"allowCustomStyle": true,
"spacing": {
"padding": 100
}
}
}
Resulting card:
As you can see, there is certainly not 100px of padding being added to the card. I've used sample host configs and tweak other settings like colors so I know the config is being applied, but nothing I do seems to affect the card padding. Thanks in advance!
Card JSON:
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": "Publish Adaptive Card schema"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Image",
"style": "Person",
"url": "https://pbs.twimg.com/profile_images/3647943215/d7f12830b3c17a5a9e4afcc370e3a37e_400x400.jpeg",
"size": "Small"
}
],
"width": "auto"
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "Matt Hidinger",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "None",
"text": "Created {{DATE(2017-02-14T06:08:39Z,SHORT)}}",
"isSubtle": true,
"wrap": true
}
],
"width": "stretch"
}
]
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "Board:",
"value": "Adaptive Card"
},
{
"title": "List:",
"value": "Backlog"
},
{
"title": "Assigned to:",
"value": "Matt Hidinger"
},
{
"title": "Due date:",
"value": "Not set"
}
]
}
]
}
],
"actions": [
{
"type": "Action.ShowCard",
"title": "Set due date",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Date",
"id": "dueDate"
},
{
"type": "Input.Text",
"id": "comment",
"placeholder": "Add a comment",
"isMultiline": true
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "OK",
"url": "http://adaptivecards.io"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}
},
{
"type": "Action.OpenUrl",
"title": "View",
"url": "http://adaptivecards.io"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
I try to render a geojson in vega.
I found this example which work fine:
How to read geojson with vega
however, when trying to replace the geojson with one of mine, the features get completely distorted.
{"$schema": "https://vega.github.io/schema/vega/v3.0.json",
"width": 500,
"height": 600,
"autosize": "none",
"signals": [
{
"name": "translate0",
"update": "width / 2"
},
{
"name": "translate1",
"update": "height / 2"
}
],
"projections": [
{
"name": "projection",
"type": "mercator",
"scale": 1000,
"rotate": [
0,
0,
0
],
"center": [
17,
-3
],
"translate": [
{
"signal": "translate0"
},
{
"signal": "translate1"
}
]
}
],
"data": [
{
"name": "drc",
"url": "https://gist.githubusercontent.com/thomas-maschler/ef9891ef03ed4cf3fb23a4378dab485e/raw/47f3632d2135b9a783eeb76d0091762b70677c0d/drc.geojson",
"format": {
"type": "json",
"property": "features"
}
}
],
"marks": [
{
"type": "shape",
"from": {
"data": "drc"
},
"encode": {
"update": {
"strokeWidth": {
"value": 0.5
},
"stroke": {
"value": "darkblue"
},
"fill": {
"value": "lightblue"
},
"fillOpacity": {
"value": 0.5
}
},
"hover": {
"fill": {
"value": "#66C2A5"
},
"strokeWidth": {
"value": 2
},
"stroke": {
"value": "#FC8D62"
}
}
},
"transform": [
{
"type": "geoshape",
"projection": "projection"
}
]
}
]
}
Here is what they are suppose to look like
https://gist.github.com/thomas-maschler/ef9891ef03ed4cf3fb23a4378dab485e
What am I getting wrong?
Thanks,
Thomas
Not sure what happened. It seems your geojson was corrupt, but not really as I eventually could parse it in www.mapshaper.org. I reduced the file to 35% and then it parsed normally:
Vega-lite spec below (compile to Vega code in the editor if needed):
{
"$schema": "https://vega.github.io/schema/vega-lite/v2.json",
"width": 700,
"height": 500,
"config": {"view": {"stroke": "transparent"}},
"layer": [
{
"data": {
"url": "https://gist.githubusercontent.com/mattijn/2ce897c2020a6e5b7ae6baf03dffe179/raw/564b6d484657864dcb77d0bb18db00fc7dc7668d/drc.geojson",
"format": {"type": "json", "property": "features"}
},
"mark": {"type": "geoshape", "stroke": "white", "strokeWidth": 1},
"encoding": {"color": {"value": "#bcbcbc"}}
}
]
}
It sounds super simple, but I can't get how can I use geojson, not topojson, for my polygons.
that's my current attempt:
"data": [
{
"name": "nabs",
"url": "both_boundaries.geojson",
"format": {"type": "json"},
"transform": [
{
"type": "geopath", "projection": "mercator",
"scale": 74, "center": [-73.99,40.72]
}
]
}
]
You have to parse the features using property within your format:
"format": {"type": "json", "property":"features"},
Full example spec:
{"$schema": "https://vega.github.io/schema/vega/v3.0.json",
"width": 500,
"height": 600,
"autosize": "none",
"signals": [
{
"name": "translate0",
"update": "width / 2"
},
{
"name": "translate1",
"update": "height / 2"
}
],
"projections": [
{
"name": "projection",
"size": {"signal": "[width, height]"},
"fit": {"signal": "data('netherlands')"}
}
],
"data": [
{
"name": "netherlands",
"url": "https://raw.githubusercontent.com/mattijn/datasets/master/NL_outline_geo.json",
"format": {
"type": "json",
"property": "features"
}
}
],
"marks": [
{
"type": "shape",
"from": {
"data": "netherlands"
},
"encode": {
"update": {
"strokeWidth": {
"value": 0.5
},
"stroke": {
"value": "darkblue"
},
"fill": {
"value": "lightblue"
},
"fillOpacity": {
"value": 0.5
}
},
"hover": {
"fill": {
"value": "#66C2A5"
},
"strokeWidth": {
"value": 2
},
"stroke": {
"value": "#FC8D62"
}
}
},
"transform": [
{
"type": "geoshape",
"projection": "projection"
}
]
}
]
}