Flow Builder Manual
The Basics
WhatsApp Commands
- Router for testing: https://wa.me/+5215548374302
- Test a Flow:
!test workflow-name
- Reset a Flow:
!reset
- Exit a Flow and back to Router:
!exit
- Jump to a specific state:
!jump stateId
Order of operations
Everything in Yalo Studio happens from top to bottom until it reaches a valid branching action from one step to another.
Saving Variables
There are two ways to save variables in Studio:
The Context
: these are key:value pairs saved temporarily for a specific user in a specific Flow and deleted when the session ends (typically 24 hours after the first message).The Profile
: these are key:value pairs saved permanently for a specific user in a specific Flow. These are encrypted by default.
Webhook Triggers
The webhook component
allows external services to communicate with Studio. Each component generates a unique URL that can be triggered when a web service sends a valid HTTP POST request to the URL of that component. When this happens, this component evaluates actions top to bottom.
To trigger this type of webhook component, you need to send a payload like the one below. The object called data below will be automatically saved in a Context variable called webhookPayload
:
-- A valid HTTP POST request with a JSON payload like the one below. The userId is mandatory; the data fields are optional.
{
"userId": "phone number including area code",
"data": {
"key": "value",
"key": "value",
"key": "value"
}
}
Given you can't edit the payload for Frontapp
, and given how typical that use case is, Yalo Studio will automatically detect if the webhook is coming from Frontapp and find the userId from their payload.

Yalo Studio - Webhook trigger
Step
Steps in Studio are made up of 3 parts:
- Message
- Trigger
- Actions
All is evaluated top to bottom.

Yalo Studio - Step
Messages
Text
- Strings: You can send any text using emojis, bold or italics.
- Handlebars: Studio supports all built-in-helpers in Handlebars
- You can add variables saved in the Context or Profile like this:
-- Context Some text with a {{variableName}} -- Profile Some text with a {{profile.variableName}}
Media
- Image
- Video
- Audio
- Document
Interactive Messages (more info here)
- Whatsapp Buttons
- Whatsapp Lists
Custom Handlebars Helpers
For the following examples we are assuming the following context is available as team.
{
"name": "Studio Core",
"company": "Yalo"
}
ifEquals
{{#ifEquals team.name 'Studio Core'}}
You are in the Studio Core team.
{{/ifEquals}}
ifNotEquals
{{#ifNotEquals team.name 'Studio Core'}}
You are not in the Studio Core team.
{{/ifNotEquals}}
stringify
{{stringify team}}
-- outputs the following
{"name":"Studio Core","company":"Yalo"}
random / opt
{{#random 3}}
{{#opt}}
Welcome!
{{/opt}}
{{#opt}}
ÂĄBienvenido!
{{/opt}}
{{#opt}}
Bem vinda!
{{/opt}}
{{/random}}
Advanced
Advanced users of Yalo Studio should leverage 3 main things:
Handlebars
in the Message area: https://handlebarsjs.com/guide/Yalo's Standard Library
: easy-to-use functions for common things.Lua
: lightweight and easy-to-learn scripting language.
Standard Libraries
These are Yalo's custom functions added to the Lua environment to facilitate various operations inside Flow Builder. The available data types available in Lua are the following. More information.
- nil
- boolean
- number
- string
- table
The method section of each standard library describes the name, the arguments, and the return value of the functions. Example explanation of the syntax
HTTP.post(url: string, body?: table, headers?: table) -> HttpResponse
How we can interpret the function?
HTTP The namemespace of the Standard Library.
post The method name.
url: string url is the expected input and string the datatype.
body?: table if the input has a ? suffix it means it is an optional argument
meaning that HTTP.post(url) would be a valid call too.
HttpResponse This is the data type that the function returns, in this case, is an
object (lua table) that contains multiple properties inside (See HTTP
Standard Library for information on this data type)
Debug
The debug library allows to send messages to WhatsApp from inside Lua code; this is to be used mostly to debug/troubleshoot issues in the execution of a Flow since using the messages section in Flow Builder provides more flexibility to handle messages.
Methods
Debug.print(text: string) -> nil
Examples
-- Send a variable content to WhatsApp
name = "Studio Core"
Debug.print(name)
-- You can also send a Lua table which would be formatted as JSON string
user = {
name = "Studio Core",
email = "[email protected]"
}
Debug.print(user)
-- Whatsapp output:
-- {"name":"Studio Core","email":"[email protected]"}
Workflow
The Flow library is the way we can move between steps in a Flow in Studio. You can use this library if you want to move to a new step while executing a Lua block. Please notice that using this function will halt the execution of the Lua code, so any code below this function will not get executed.
This function needs to be in the outer scope of the Lua code script. You cannot call Workflow.branch inside a Lua function because it wonât halt the execution. Please notice the second example to understand this rule.
Methods
Workflow.branch(stepId: number) -> nil
Examples
age = 20
-- We want to move to a Step 1 if the age is equals or greater than 18
-- else we want to move to a Step 2 if its lower than 18.
if age >= 18 then
Workflow.branch(1)
else
Workflow.branch(2)
end
-- THIS WON'T WORK
-- If you call Workflow.branch inside a function, the execution won't be halted.
-- In the following example, the script would exit branching to step id 2.
function customFunction(age)
if age >= 18 then
Workflow.branch(1)
end
Workflow.branch(2)
end
customFunction(18)
JSON
The JSON library helps with transforming between JSON strings and Lua tables.
Methods
JSON.encode(object: table) -> string
JSON.decode(jsonString: string) -> table
Examples
user = {
name = "Studio Core",
email = "[email protected]"
}
userJsonString = JSON.encode(user)
-- userJsonString
-- {"name":"Studio Core","email":"[email protected]"}
userJsonString = '{"name":"Studio Core","email":"[email protected]"}'
user = JSON.decode(userJsonString)
Debug.print(user.name) -- Would send "Studio Core"
HTTP
The HTTP library is a wrap of the Javascript Axios Library. This is the library we would use if we need to connect to an external service from Flow Builder.
Methods
HTTP.get(url: string, headers?: table) -> HttpResponse
HTTP.post(url: string, body?: table, headers?: table) -> HttpResponse
HTTP.put(url: string, body?: table, headers?: table) -> HttpResponse
HTTP.patch(url: string, body?: table, headers?: table) -> HttpResponse
HTTP.delete(url: string, headers?: table) -> HttpResponse
HttpResponse
{
data: table,
status: number -- 200
statusText: string -- OK
headers: table,
config: table { url: string, method: string, data: table, headers: table }
}
Examples
-- API that returns a random generated user
url = "https://randomuser.me/api/"
-- Make the HTTP Get request
response = HTTP.get(url)
-- Notice Lua first index is 1 not 0.
user = response.data.results[1]
-- We can send the name to WhatsApp
Debug.print(user.name.first .. " " .. user.name.last)
-- Fictional REST API to handle users
url = "https://external-api-service.net/v1/users"
body = {
name = "Studio Core",
email = "[email protected]"
}
headers = {
authorization = "Bearer securitytoken",
[content-type] = "application/json" -- Notice the use of [ ] is the variable has
} -- invalid characters like ( - )
-- Make the HTTP Post request with body and headers
response = HTTP.post(url, body, headers)
if response.status == 201 then
Debug.print("The user has been created")
else
Debug.print("There was a problem creating the user")
-- You could inspect the contents of response.data which
-- could contain the error message that caused it to fail
Invoke
We can use AWS lambdas by using the invoke library. Currently, we only support lambdas inside of Yalo's AWS account but eventually we'll support other providers and other accounts.
Methods
Invoke.cloudFunction(lambda: string, body: string) -> ServerlessResponse
ServerlessResponse
{
success: boolean -- True if the lambda was executed successfully
payload: table -- Response from the lambda
}
Examples
-- Name of the Lambda
lambda = 'add-product-shopping-cart'
-- Body to the Lambda
body = {
name = "Studio NG",
price = 9999
}
-- Since the body of the lambda has to be a string we
-- need to convert our body to a JSON string.
result = Invoke.cloudFunction(lambda, JSON.encode(body))
if result.success then
Debug.print(result.payload) -- This would send the lambda response to WhatsApp.
else
Debug.print("There was a problem executing the lambda")
end
Input
The input library helps on matching strings using different methods like custom regexes or simplifying the process by just passing a list of values to match against another value. This also provides a way to retrieve the user profile information and the latest user message details.
Methods
Input.match(regex: string, expression: string,
value?: string, options?: table) -> boolean
Input.intent(domain: string, message: string) -> IntentResponse
Input.getMessage() -> ContextMessage
Input.getProfile() -> ContextProfile
-- Optional Configuration Options for match
options
{
toLower = true,
removeDiacritics = true, -- e.g. hĂślĂĄ --> hola
reduceRepeatedAlphanumerics = true -- If you have 3 or more repeated chars, it reduces them to 2 repeated chars. e.g. aaaa -> aa
}
IntentResponse
{
status: string
message: string
domain?: string
categories?: table
confidence?: table
}
ContextMessage
{
message: string
type: string -- text, interactive, image, video, file, location, audio
step: string -- current step name
}
ContextProfile
{
userId: string
name: string
}
Examples
-- Check if a value exists in a list of keywords
match = Input.match('contains', 'blue|red', 'blue')
-- match would be True.
-- Check if a value matches to a custom regex
-- Using a phone regex which accepts only numbers, () and -
match = Input.match('regex', '^[0-9()-]+$', '(656) 123-1234')
-- match would be True.
-- Check if the value is equal to the other value
match = Input.match('isEqualTo', 'Studio NG', 'Other Product')
-- match would be False.
-- Explicitly state the value to compare
Input.intent(domain: string, value: string)
-- Implicitly infer the value to compare from the last user message
Input.intent(domain: string)
-- Example the explicit way
-- We use generic-en domain for the intent.
intent = Input.intent('generic-en', 'hello!')
if intent.confidence.greeting > .75 then
Debug.print("We understand you are doing a greeting intent")
else
Debug.print("Not a greeting intent")
end
-- More info on Intents here
-- https://www.notion.so/yalo/Users-Pre-Configured-Intents-Details-81e7c065e70d431d992ca7f68e00941e
input = Input.getMessage()
Debug.print(input.message) -- Would send the latest user message to whatsapp.
profile = Input.getProfile()
Debug.print("Hello " .. profile.name .. " your phone is " .. profile.userId)
Context
The context library gives us access to the ephemeral context of the current user (brain) in which we can store information that is going to live through the lifespan of the Flow session, which is defined in the Flow configuration in Flow Builder. The information is stored in a key:value datastore in which we can store JSON serialized objects too.
Methods
Context.set(key: string, value: string|table) -> nil
Context.get(key: string) -> string
Context.delete(key: string) -> nil
Context.check(key: string) -> boolean
Examples
email = "[email protected]"
Context.set("email", email)
email = Context.get("email")
Debug.print(email) -- Would send [email protected] to whatsapp
-- Retrieving JSON string from the Context
-- and convert it back to a Lua table
-- Context (user) = '{"name":"Studio Core","email":"[email protected]"}'
userJsonString = Context.get("user")
-- Convert to Lua table using JSON (See JSON Documentation above)
user = JSON.decode(userJsonString)
-- This would send "Studio Core"
Debug.print(user.name)
Context.delete("email") -- Would delete the value from the user context
Context.set("email", "[email protected]")
if Context.check("email") then
Debug.print("Email exists in the Context")
end
Profile
The profile library is very similar to the context library. The main difference is that the information stored with the profile library does not expire, and it is encrypted in the data store. The primary use for this is information that would span across multiple Flow sessions, like a user address.
Methods
Profile.set(key: string, value: string) -> nil
Profile.get(key: string) -> string
Profile.delete(key: string) -> nil
Profile.check(key: string) -> boolean
Examples
address = "Studio Avenue 123"
Profile.set("address", address)
address = Profile.get("address")
Debug.print(address) -- Would send "Studio Avenue 123"
Profile.delete("address") -- Would delete the value from the user profile
Profile.set("addresss", "Studio Avenue 123")
if Profile.check("address") then
Debug.print("Address exists in the Context")
end
FAQs using Sheets
The FAQ library uses the Sheets feature of Flow Builder to provide an easy access to the questions and the result from querying a question against the multiple available questions.
Methods
FAQ.searchQuestion(question: string, sheetId: string, config?: table) -> FAQResponse
config
{
outputAttr: string -- Name of the key in which the response is stored
numAnswers: number -- Number of answers returned from the service
minSingleSimilarity: number -- Threshold for single matching, default 0.5
minMultipleSimilarity: number -- Threshold for multiple matching, default 0.2
lambdaScript: string -- The AWS lambda to process the FAQ matching
}
-- Success Response
FAQResponse
{
success: boolean -- True if the lambda execution for FAQs was successful
singleMatch: boolean -- True if there was an exact match for the question
multipleMatch: boolean -- True if multiple questions were matched
noMatch: boolean -- True if there was no match for the question
searchId: string -- The search id returned by the serverles semantic search
documentId: string -- The document id from the search results
faqs: FAQsQuestions -- The list of all the matching questions
matchQuestion: string -- If the response is singleMatch this contains the matched question. Or N/A if no answers where found.
matchAnswer: string -- If the response is singleMatch this contains the matched answer. Or N/A if no answers were found.
}
-- Error Response
FAQResponse
{
success: false
errorMessage: string -- The details of the error
}
FAQsQuestions
{
index: number
indexEmoji: string
title: string
content: string
}
Examples
sheetId = '61258430ca635768d3e07932'
-- This will send the latest user message as a question to the FAQs
-- table specified in the second argument.
result = FAQ.searchQuestion("What do you sell?", sheetId)
-- Check if the FAQ Lambda resolved correctly
if not result.success then
-- We can output the error message
-- during debugging process
Debug.print(result.errorMessage)
-- Move to a fallback step to handle
-- the FAQ not being available.
Workflow.branch(11)
end
-- Different Scenarios
-- If the question has a single match
if result.singleMatch then
-- Store the Question/Answer in Context
Context.set("question", result.matchQuestion)
Context.set("answer", result.matchQuestion)
-- Move to a Step where the Question/Answer will
-- be displayed using handlebars, Ex.
-- Q: {{question}}
-- A: {{answer}}
Workflow.branch(13)
end
-- If there were mutliple questions matched
if result.multipleMatch then
-- Store the matched questions/answers in Context
Context.set("questions", result.faqs)
-- Move to a Step where we are going to display the
-- matching questions/answers with handlebars, Ex.
-- {{#each questions}}
-- {{index}}) {{title}}
-- {{/each}}
Workflow.branch(14)
end
-- If there was no match for the question.
if result.noMatch then
-- Move to a Step where you notify the user
-- that there were no matches for this question.
Workflow.branch(15)
end
-- After displaying the multiple matched questions to the user
-- we need to ask the user for the index of the question he meant
-- We retrieve the user message
message = Context.get("userMessage")
-- We get convert to a number
selection = String.onlyNumbers(message)
-- Retrieve the questions from the Context
-- We assume they were stored under "questions"
questions = Context.get("questions")
-- We convert the JSON to a Lua Table
questions = JSON.decode(questions)
-- We iterate all the questions to see if the index
-- provided by the user exists in the available questions
for key, question in pairs(questions) do
-- If we find a match we can store the question/answer
-- and move the user to the Step we use for single match
if question.index == selection then
Context.set("question", question.title)
Context.set("answer", question.content)
-- We can use the same Step as the one we
-- use for single match in the previous example
Workflow.branch(14)
end
end
-- OPTIONAL.
-- We can provide a fallback here since
-- the user answer didn't match any index
-- of the questions like moving to another Step.
Workflow.branch(1)
Location
The location library wraps the functionality for store locator logic. You can search for locations of a store/place using the library and display back the results to the users using handlebars.
Methods
Location.stores(keyword: string, radius: number,
placeType: string, address: string) -> LocationResult
LocationResult
{
success: boolean -- True if there was something matched
places: LocationDetail
}
LocationDetail
{
address_components: AddressComponent[]
adr_address: string
business_status: string
formatted_address: string
formatted_phone_number: string
geometry: Geometry
icon: string
international_phone_number: string
name: string
opening_hours: ExtendedBusinesHours[]
photos: Photo[]
place_id: string
plus_code: Plus
price_level: number
rating: number
reference: string
reviews: Review[]
types: PlaceType[]
url: string
user_ratings_total: number
utc_offset: number
vicinity: string
website: string
}
AddressComponent
{
long_name: string
short_name: string
types: string
}
ExtendedBusinessHours
{
weekday_text: string[]
periods {
close {
day: number
time: string
}
open {
day: number
time: string
}
}
}
Examples
-- Location stores has 2 possible signatures
-- Explicitly state the address you want to nearby search
stores = Location.stores(keyword, radius, PlaceType, address)
-- Implicitly infer the address value from user's last message
stores = Location.stores(keyword, radius, PlaceType)
if stores.success then
-- Storing the results in storesFound context
Context.set("storesFound", stores.places)
else
Debug.print("We did not find any location/store")
end
-- You can use handlebars in another step to display this information
-- Step Message
{{#each storesFound}}
đ{{name}}
đ{{url}}
đşď¸{{formatted_address}}
đ{{formatted_phone_number}}
{{/each}}
String
Methods
String.cleanText(text: string) -> string
String.onlyNumbers(text: string) -> number
String.split(text: string) -> table
String.encodeURI(text: string) -> string
String.formatCurrency(value: string) -> string
Examples
clean = String.cleanText(string)
-- e.g. "äDiós"
-- returns "aDios"
number = String.onlyNumbers(string)
-- e.g. "my age is 25"
-- returns "25"
-- e.g. "my age is 25 next friday the 20th"
-- returns "2520"
message = 'Apple iPhone 13 Pro Max'
words = String.split(message)
-- words = {'Apple', 'iPhone', '13', 'Pro', 'Max'}
message = 'Apple iPhone 13 Pro Max'
words = String.split(message, 'Pro')
-- words = {'Apple iPhone 13 ', ' Max'}
message = 'Apple iPhone 13 Pro Max'
urlString = String.encodeURI(message)
-- urlString = Apple%20iPhone%2013%20Pro%20Max
value = '123'
currency = String.formatCurrency(value)
-- currency = '$123.00'
value = '12345,6'
currency = String.formatCurrency(value)
-- currency = '$12,345.60'
value = '123.45'
currency = String.formatCurrency(value, "pt-br", "BRL")
-- currency = 'R$ 123,45'
value = '123.45'
currency = String.formatCurrency(value, "es-mx", "MXN")
-- currency = '$123.45'
Schedule
This library provides the ability to trigger a message or a transition after a given delay of no activity. If the user has scheduled events but sends a message before those delays expire, they will be canceled. You can have as many scheduled events as needed; they are queued correctly based on the provided delay in minutes.
Methods
Schedule.message(stepId: number, delayInMinutes: number)
Schedule.branch(stepId: number, delayInMinutes: number)
Examples
-- Assuming there is a Step with Id 15.
-- The Step 15 has a text message: "Are you there?"
Schedule.message(15, 1)
-- After one minute of running this code the user
-- will receive a message: "Are you there?"
-- But the user will stay on the same step it was.
Schedule.branch(15, 5)
-- After 5 minutes of running this code the user
-- will be branched to the Step 15 and will also
-- receive the message: "Are you there?"
Business Hours
This library is a wrapper to make working with open and close schedules for stores easier. It accepts a simple configuration of daily schedule for a store and returns if the store is open/close at the moment of the execution.
Methods
BusinessHours.config(options: BusinessHoursOptions) -> StoreStatus
BusinessHoursOptions {
timezone = "America/Denver" -- https://timezonedb.com/time-zones
monday = "09:00-17:00" -- Format is always 2 digits numbers for hours/minutes.
tuesday = "09:00-17:00"
wednesday = "09:00-17:00"
thursday = "09:00-17:00"
friday = "09:00-17:00"
saturday = "closed" -- You can also configure the store as "closed"
sunday = "closed"
}
StoreStatus {
error = nil -- Will be nil when no error or string with error information.
open = true -- Boolean true if the store is open.
}
Examples
-- Create the configuration options
options = {
timezone = "America/Denver",
monday = "09:00-17:00",
tuesday = "09:00-17:00",
wednesday = "09:00-17:00",
thursday = "09:00-17:00",
friday = "09:00-17:00",
saturday = "closed",
sunday = "closed"
}
-- For this example we are assuming the current date is Monday 10 AM
storeState = BusinessHours.config(options)
-- Always check first if there was no error.
-- Errors are always related to configuration, ex:
-- Invalid timezone (See available options in the list).
-- Missing information for a day (Ex. Monday with no schedule).
-- Invalid format for schedule has to be 00:00-00:00 or 'closed'.
if storeState.error then
Debug.print(storeState.error)
end
-- We now can safely confirm if the store is open/closed
if storeState.open then
Debug.print("The store is OPEN")
else
Debug.print("The store is CLOSED")
end
Commerce SDK
This library is a wrapper to make requests to Commerce API's (Headless API).
Methods
** NOTE **
These methods are missing the response schema for âHeadless Responseâ.
Include the required parameters and their data type.
Create a Lua example for the different use cases of these functions and also âbest practicesâ to follow like error handling when the response is not the expected.
Commerce.init(name: string, url?: string) -> headlessResponse
Commerce.sessionCreate(type: 'code' | 'phoneNumber', value: string) -> headlessResponse
Commerce.cartGet() -> headlessResponse
Commerce.orderCreate() -> headlessResponse
Commerce.orderConfirm(orderId: string) -> headlessResponse
Commerce.sessionExpire() -> headlessResponse
-- Missing methods from the SDK
Commerce.checkoutRuleAccept()
Commerce.checkoutRuleGet()
Commerce.checkoutRuleSet()
Commerce.orderCancel()
Commerce.orderCancel()
Commerce.orderCheck()
Commerce.orderGet()
Commerce.cartEmpty()
Commerce.cartRemove()
Commerce.search()
Commerce.phoneNumberAdd()
Commerce.storefrontGet()
Commerce.cartAdd()
Commerce.phoneNumberRemove()
Commerce.addressUpdate()
Commerce.catalog()
Commerce.productGet()
Commerce.sessionCreate()
Commerce.storeNearby()
Examples
storefrontName = 'my-storefront'
storefrontUserUrl = 'https://my-headless-api.com'
-- by default Flow Builder will use the production headless
-- if you want to use another env send the second argument
-- if you want to use the production env, just send the storefrontName
initResponse = Commerce.init(storefrontName, storefrontUserUrl)
-- {
-- "status": "ok",
-- "data": {
-- "storefrontName": "my-storefront"
-- }
-- }
if initResponse.status == 'ok' then
-- success step id
Workflow.branch()
end
-- something went wrong step id
Workflow.branch()
type = 'code' -- this also could be 'phoneNumber'
endUserDocument = '123456789'
sessionResponse = Commerce.sessionCreate(type, endUserDocument)
-- {
-- "status": "ok",
-- "data": {
-- "id": "62d096e623e6fd4021966afe",
-- "customFields": null,
-- "workflow": {
-- ...
-- },
-- "configuration": {
-- "checkoutRules": {
-- ...
-- }
-- },
-- "customer": {
-- ...
-- }
-- }
-- }
if sessionResponse.status == 'ok' then
-- success step id
Workflow.branch()
end
-- something went wrong step id
Workflow.branch()
cartResponse = Commerce.cartGet()
-- {
-- "status": "ok",
-- "data": {
-- "id": "62d096e623e6fd4021966afb",
-- "items": [{
-- ...
-- }],
-- "total": 98.5600004196167,
-- "status": "IN_PROGRESS",
-- "warnings": null,
-- "customFields": null
-- }
-- }
if cartResponse.status == 'ok' then
-- success step id
Workflow.branch()
end
-- something went wrong step id
Workflow.branch()
orderResponse = Commerce.orderCreate()
-- {
-- "status": "ok",
-- "data": {
-- "id": "62d096ff23e6fd4021966b69",
-- "customerCode": "38313220000148",
-- "storeCode": null,
-- "items": [
-- {
-- ...
-- }
-- ],
-- "status": "CREATED",
-- "processedAt": null,
-- "externalRef": null,
-- "parentOrderUid": null,
-- "notes": null,
-- "externalErrorMessage": null,
-- "externalMessage": null,
-- "source": null,
-- "customFields": null,
-- "total": 98.5600004196167,
-- "sequence": 0,
-- "checkoutRules": [],
-- "postOrderCheck": null,
-- "preOrderCheck": null,
-- "activePromotions": [],
-- "createdAt": {
-- ...
-- },
-- "updatedAt": {
-- ...
-- }
-- }
-- }
Context.set('orderResponse', orderResponse)
if orderResponse.status == 'ok' then
-- success step id
Workflow.branch()
end
-- something went wrong step id
Workflow.branch()
orderResponse = JSON.decode(Context.get('orderResponse'))
orderId = orderResponse.data.id
orderConfirmedResponse = Commerce.orderConfirm(orderId)
-- same orderCreate() response with different status
-- {
-- "status": "ok",
-- "data": {
-- ...
-- "status": "CONFIRMED",
-- ...
-- }
-- }
if orderConfirmedResponse.status == 'ok' then
-- success step id
Workflow.branch()
end
-- something went wrong step id
Workflow.branch()
sessionResponse = Commerce.sessionExpire()
-- {
-- "status": "ok",
-- "data": {
-- "id": "62d096e623e6fd4021966afe",
-- "customFields": null,
-- "status": "EXPIRED",
-- "cartUid": "62d096e623e6fd4021966afb",
-- "workflow": {
-- ...
-- }
-- }
-- }
if sessionResponse.status == 'ok' then
-- success step id
Workflow.branch()
end
-- something went wrong step id
Workflow.branch()
Lua Advanced / Standard Library Guides
Create WhatsApp lists with external HTTP API data
Lua Basics
Yalo Studio's advanced actions are written in Lua, an easy-to-learn scripting language. The Lua documentation itself is at https://www.lua.org/docs.html, although it's not all that approachable. This intro should help you with enough to get going.
Printing
There are two ways to print in the Advanced Actions. The standard Lua print
is good to know, and this will probably print to Studio in future versions. Debug.print
prints to the channel (WhatsApp, for instance.)
print("Hello World") -- regular print in Lua (not available in Studio yet)
Debug.print("Hello world") -- send this message to the channel
Comments
Comments in Lua are a bit different from other programming languages and start with a --
--this is a comment
print("hello") --this is another comment
-- the next line will not do anything because it is commented out
--print("world")
Variables
Lua has the usual types such as numbers and strings. The word local
at the beginning is not strictly necessary but is recommended to help Yalo Studio understand the program. An important data type is nil
, which represents "no value".
local x = 10 --number
local name = "john doe" --string
local isAlive = false -- boolean
local a = nil --no value or invalid value
Operators
For numbers, math operators are as follows:
+
addition-
minus*
multiply/
divide^
power%
modulus
-- examples
local a = 1
local b = 2
local c = a + b
print(c) -- 3
local d = b - a
print(d) -- 1
local x = 1 * 3 * 4 -- 12
print(x)
local y = (1+3) * 2 -- 8
print(y)
print(10/2) -- 5
print (2^2) -- 4
print(5%2) -- 1
print(-b) -- -2
String concatenation is with two dots (..
).
-- concatenate strings
local phrase = "My name is "
local name = "John Doe"
print(phase .. name) -- My name is John Doe
-- strings and numbers
local age = 12
local name = "Billy"
print(name .. " is " .. age .. " years old") -- Billy is 12 years old
Booleans
Booleans are used for yes/no values.
local isAlive = true
print(isAlive) --true
isAlive = false
print(isAlive) --false
Conditional statements
A conditional statement is for splitting up the execution depending on a condition. A conditional statement looks at a condition (a block of text) between if
and then
, and executes the following block of text until the end
word only if that condition is true. It's easier to see how it works by looking at an example.
--number comparisions
local age = 10
if age < 18 then
print("over 18") --this will not be executed
end
--elseif and else
age = 20
if age > 18 then
print("dog")
elseif age == 18 then
print("cat")
else
print("mouse")
end
Comparison operators
To see if two things (strings, numbers, etc.) are equal, you use ==
. (Not =
!) To if they are not equal, you use ~=
.
==
equality<
less than>
greater than<=
less than or equal to>=
greater than or equal to~=
inequality
Combining statements
Statements can be combined by using and
:
local x = 10
if x == 10 and x < 0 then --both are true
print("dog")
elseif x == 100 or x < 0 then --1 or more are true
print("cat")
end
Nested statements
You can have one if
statement inside another if
statement:
local x = 10
local isAlive = true
if x==10 then
if isAlive == true then
print("dog")
else
print("cat")
end
end
Invert value
You can invert the logic of the match by using not
:
local x = 10
if not x == 10 then
print("here")
end
Functions
You can group your code into functions in Studio. However, functions can at the moment only be used within the same action and are not reusable across the rest of the Flow. (This might change.)
function printTax(price)
local tax = price * 0.21
print("tax:" .. tax)
end
printTax(200)
Functions may return values that you can use in other calculations.
--function that returns a value
function calculateTax(price)
return price * 0.21
end
local result = calculateTax(100)
print(result)
--reusing the function but this time using variables
local bread = 130
local milk = 110
local breadTax = calculateTax(bread) --27.3
local milkTax = calculateTax(milk) --23.1
print("Bread Tax = " .. breadTax)
print("Milk Tax = " .. milkTax)
Functions may take multiple parameters. They can only have one return value. (Although you can always return a table of values - see "tables" below.)
--multiple parameters
function displayInfo(name, age, country)
print(name .. " is " .. age .. " years old and is from " .. country)
end
displayInfo("Billy", 12, "Jupiter")
Variable scope
Variables are only valid inside the block where they are declared. In this case, the variable "a" is only available inside the function. Once outside the function, its value is lost. (We say that it's no longer "in scope")
function foo()
local a = 10
end
print(a) --nil
Tables
A table is a list-like structure that we can use to store information. To access a particular item in a table, we use square brackets with the element index (the item number from left to right), as shown in the following example. The number of elements in a list is available by using the #
symbol next to the table name.
--basic table
local colors = { "red", "green", "blue" }
print(colors[1]) --red
print(colors[2]) --green
print(colors[3]) --blue
--using a loop to iterate though your table
for i=1, #colors do
print(colors[i])
end
We can insert values at the end of a table using insert
--insert
local colors = { "red", "green", "blue" }
table.insert(colors, "orange")
local index = #colors --4 (this is the last index in the table)
print(colors[index]) --orange
If we give insert
a number as the second argument, this number is stored at a specific position in the list.
--insert at index
local colors = { "red", "green", "blue" }
table.insert(colors, 2, "pink")
for i=1, #colors do
print(colors[i])
end
--red, pink, green, blue
--remove
local colors = { "red", "green", "blue" }
table.remove(colors, 1)
for i=1, #colors do
print(colors[i])
end
-- "green", "blue"
Key tables
A key table is like a dictionary that helps you store things by name, and then retrieve them later.
local teams = {
["teamA"] = 12,
["teamB"] = 15
}
print(teams["teamA"]) -- 12
for key,value in pairs(teams) do
print(key .. ":" .. value)
end
Insert an item into a key table
--insert into key table
teams["teamC"] = 1
--remove key from table
teams["teamA"] = nil
function getTeamScores()
local scores = {
["teamA"] = 12,
["teamB"] = 15
}
return scores
end
local scores = getTeamScores()
local total = 0
for key, val in pairs(scores) do
total += val
end
print("Total score of all teams:" .. total)
Math
Math is available using the math
module, which is built into Yalo Studio.
abs (absolute value)
local x = -10
print(math.abs(x)) --result: 10
local a = 10
print(math.abs(a)) --result: 10
ceil (round up a decimal value)
local x = 1.2
print(math.ceil(x)) --result: 2
floor (round down a decimal value)
local x = 1.2
print(math.floor(x)) --result: 1
pi (the constant)
print(math.pi) --3.1415926535898
3.1415926535898
random (random number generation)
--random value between 0 and 1
print(math.random()) --result: 0.0012512588885159
--random integer value from 1 to 100 (both inclusive)
print(math.random(100)) --result: 20
--random integer value from 20 to 100 (both inclusive)
print(math.random(20, 100)) --result: 54
sqrt (square root)
print(math.sqrt(100)) --result: 10
Appendix
Template Std Library (Restricted)
Interpreter Security Mechanisms (Restricted)
Updated 12 months ago