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


Steps in Studio are made up of 3 parts:

  • Message
  • Trigger
  • Actions

All is evaluated top to bottom.


Yalo Studio - Step



  • 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}}


  • 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 team.name 'Studio Core'}}
You are in the Studio Core team.


{{#ifNotEquals team.name 'Studio Core'}}
You are not in the Studio Core team.


{{stringify team}}

-- outputs the following
{"name":"Studio Core","company":"Yalo"}

random / opt

{{#random 3}}
		Bem vinda!


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)


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.


Debug.print(text: string) -> nil


-- Send a variable content to WhatsApp
name = "Studio Core"

-- You can also send a Lua table which would be formatted as JSON string
user = {
  name = "Studio Core",
  email = "[email protected]"


-- Whatsapp output:
-- {"name":"Studio Core","email":"[email protected]"}


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.


Workflow.branch(stepId: number) -> nil


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
-- 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




The JSON library helps with transforming between JSON strings and Lua tables.


JSON.encode(object: table) -> string
JSON.decode(jsonString: string) -> table


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"


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.


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

  data: table,
  status: number -- 200
  statusText: string -- OK
  headers: table,
  config: table { url: string, method: string, data: table, headers: table }


-- 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")
  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


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.


Invoke.cloudFunction(lambda: string, body: string) -> ServerlessResponse

  success: boolean -- True if the lambda was executed successfully
  payload: table -- Response from the lambda


-- 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.
	Debug.print("There was a problem executing the lambda")


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.


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
  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

  status: string
  message: string
  domain?: string
  categories?: table
  confidence?: table

  message: string
  type: string -- text, interactive, image, video, file, location, audio
  step: string -- current step name

  userId: string
  name: string


-- 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")
  Debug.print("Not a greeting intent")

-- 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)


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.


Context.set(key: string, value: string|table) -> nil
Context.get(key: string) -> string
Context.delete(key: string) -> nil
Context.check(key: string) -> boolean


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"
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")


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.


Profile.set(key: string, value: string) -> nil
Profile.get(key: string) -> string
Profile.delete(key: string) -> nil
Profile.check(key: string) -> boolean


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")

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.


FAQ.searchQuestion(question: string, sheetId: string, config?: table) -> FAQResponse

  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
  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
  success: false
  errorMessage: string -- The details of the error

  index: number
  indexEmoji: string
  title: string
	content: string


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

  -- Move to a fallback step to handle
  -- the FAQ not being available.

-- 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}}

-- 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}}

-- 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.
-- 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

-- We can provide a fallback here since
-- the user answer didn't match any index
-- of the questions like moving to another Step.


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.


Location.stores(keyword: string, radius: number, 
                placeType: string, address: string) -> LocationResult

  success: boolean -- True if there was something matched
  places: 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

	long_name: string
  short_name: string
  types: string

	weekday_text: string[]
	periods {
	    close {
	        day: number
	        time: string
	    open {
	        day: number
	        time: string


-- 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)
	Debug.print("We did not find any location/store")

-- You can use handlebars in another step to display this information
-- Step Message
{{#each storesFound}}



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


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'


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.


Schedule.message(stepId: number, delayInMinutes: number)
Schedule.branch(stepId: number, delayInMinutes: number)


-- 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.


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.


-- 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

-- We now can safely confirm if the store is open/closed
if storeState.open then
	Debug.print("The store is OPEN")
  Debug.print("The store is CLOSED")

Commerce SDK

This library is a wrapper to make requests to Commerce API's (Headless API).


** 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


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
-- something went wrong step id
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
-- something went wrong step id
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
-- something went wrong step id
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
-- something went wrong step id
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
-- something went wrong step id
sessionResponse = Commerce.sessionExpire()
-- {
--   "status": "ok",
--   "data": {
--     "id": "62d096e623e6fd4021966afe",
--     "customFields": null,
--     "status": "EXPIRED",
--     "cartUid": "62d096e623e6fd4021966afb",
--     "workflow": {
--         ...
--     }
--   }
-- }

if sessionResponse.status == 'ok' then
		-- success step id
-- something went wrong step id

Lua Advanced / Standard Library Guides

Create WhatsApp lists with external HTTP API data

Dynamic WhatsApp list

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.


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 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


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


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

local y = (1+3) * 2 -- 8

print(10/2) -- 5
print (2^2) -- 4
print(5%2) -- 1

print(-b) -- -2
ℹ️ local level = 1 level = level + 1 print(level) -- 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 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

--elseif and else
age = 20
if age > 18 then
elseif age == 18 then

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
elseif x == 100 or x < 0 then --1 or more are true

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

Invert value

You can invert the logic of the match by using not:

local x = 10
if not x == 10 then


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)


Functions may return values that you can use in other calculations.

--function that returns a value
function calculateTax(price)
  return price * 0.21

local result = calculateTax(100)

--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)

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

print(a) --nil


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.

ℹ️ **Lua is 1-indexed** Unlike in other programming languages, element indices start at 1.
--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

We can insert values at the end of a table using 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
--red, pink, green, blue
local colors = { "red", "green", "blue" }
table.remove(colors, 1)
for i=1, #colors do
-- "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)

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

local scores = getTeamScores()
local total = 0
for key, val in pairs(scores) do
  total += val
print("Total score of all teams:" .. total)


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

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


Template Std Library (Restricted)

Interpreter Security Mechanisms (Restricted)