Turn.io Lua Apps are currently part of an early access program and are not yet generally available. If you are interested in building custom applications and would like to participate, please reach out to our support team to discuss your use case.
Building Lua Apps on Turn.io
Welcome to the developer guide for building Lua Apps on the Turn.io platform! This guide will walk you through everything you need to know to create, test, and deploy powerful applications that extend the capabilities of Turn.
What are Turn Lua Apps?
Turn Lua Apps are self-contained packages of Lua code that run securely within the Turn platform. They allow you to implement custom business logic that can be triggered by platform events, incoming HTTP requests, or directly from a Journey.
With Lua Apps, you can:
- Integrate with external APIs and third-party systems.
- Create complex, stateful workflows that go beyond standard Journey capabilities.
- Build custom backend logic for your messaging services.
- Handle asynchronous events, like waiting for a payment confirmation webhook.
Getting Started: Your First App
The best way to start is by using our official app template, which includes a basic app structure, a testing framework, and a packaging script.
-
Download the Template: Get a copy of the
lua_app_template
directory. -
Project Structure: Your new app directory will look like this:
my_app/
├── my_app.lua # Your main application file
├── my_app-0.1.0-0.rockspec # For managing testing dependencies
├── package.sh # Script to package your app for upload
├── turn.lua # A mock of Turn APIs for local testing
└── spec/
└── my_app_spec.lua # Your test file -
Install Testing Tools: You'll need Lua and LuaRocks to run tests locally.
# On macOS with Homebrew
brew install lua luarocks
# On Debian/Ubuntu
sudo apt-get install lua5.3 luarocks -
Install Test Dependencies: Navigate to your app's directory and run:
luarocks install --only-deps my_app-0.1.0-0.rockspec
This will install Busted (a testing framework) and other helpers.
You are now ready to start developing your app!
The on_event
Function
The on_event
function in your main .lua
file is the heart of your application. It's the single entry point that receives and routes all events from the Turn platform.
Your function will receive four arguments:
app
: A table containing your app's instance configuration, including its unique UUID.number
: A table with information about the number where the app is installed.event
: A string identifying the type of event (e.g.,"install"
,"http_request"
).data
: A table containing data specific to that event.
local App = {}
local turn = require("turn")
function App.on_event(app, number, event, data)
if event == "install" then
-- Perform setup when the app is installed
turn.logger.info("My app has been installed!")
return true -- Return true on success
elseif event == "uninstall" then
-- Perform cleanup when the app is uninstalled
turn.logger.info("My app has been uninstalled.")
return true
elseif event == "contact_changed" then
-- React to a contact being updated
local contact_uuid = data.uuid
turn.logger.info("Contact " .. contact_uuid .. " was updated.")
return true
elseif event == "http_request" then
-- Handle an incoming HTTP request to your app's unique endpoint
return true, { status = 200, body = "Hello from my app!" }
else
-- It's good practice to handle unknown events
turn.logger.warning("Received unknown event: " .. event)
return false
end
end
return App
Integrating with Journeys
One of the most powerful features of Lua Apps is their ability to integrate directly with Journeys using the app()
block. This allows you to add custom logic, calculations, or API calls right in the middle of a conversation.
Calling an App from a Journey
In your Journey you can call an app function like this:
card GetWeather do
# Calls the 'get_forecast' function in the 'weather_app'
weather_data = app("weather_app", "get_forecast", ["Cape Town"])
# The result is available in the 'weather_data' variable
text("The weather in Cape Town is: @(weather_data.result.temperature)°C")
end
This triggers a journey_event
in your Lua app.
Handling a journey_event
Your app must handle the journey_event
and can control the flow of the Journey by its return value.
Synchronous Flow: continue
For operations that complete instantly, return "continue"
along with the result. The Journey will proceed to the next block without pausing.
- Use cases: Data validation, simple calculations, formatting text.
- Return signature:
return "continue", result_table
-- In your on_event function
elseif event == "journey_event" and data.function_name == "add" then
local sum = tonumber(data.args[1]) + tonumber(data.args[2])
-- The Journey continues immediately with the result
return "continue", { value = sum }
end
Asynchronous Flow: wait
and turn.leases
For long-running tasks, like waiting for a payment confirmation webhook, you can tell the Journey to pause by returning "wait"
.
The Journey will remain paused until your app explicitly resumes it by sending data to its lease. A lease is a temporary hold on the Journey's state, identified by the chat_uuid
.
- Use cases: Waiting for webhooks, human approvals, or timed delays.
- Return signature:
return "wait"
Example Workflow: Waiting for a Payment Webhook
-
Journey Initiates and Waits: The Journey calls your app, which initiates a payment and tells the Journey to wait.
-- journey_event handler
if data.function_name == "waitForPayment" then
-- The app might call an external payment API here
-- ...
-- Now, tell the Journey to pause
return "wait"
end -
External Webhook Arrives: Later, your payment provider sends a webhook to your app's HTTP endpoint. Your app's
http_request
handler parses it. -
App Resumes the Journey: Inside the
http_request
handler, you useturn.leases.send_input()
with the originalchat_uuid
to wake up the correct Journey and deliver the result.-- http_request handler
-- Assume you got the chat_uuid from the webhook's metadata
local chat_uuid = webhook_payload.metadata.chat_uuid
local result_data = {
payment_confirmed = true,
transaction_id = "txn_123"
}
turn.leases.send_input(chat_uuid, result_data)
The Journey receives the result_data
and automatically resumes execution.
API Reference
The turn
global table provides sandboxed access to platform features.
-
turn.assets
Load static files (like templates or images) from an
assets/
folder in your app's.zip
file.list(directory_path)
: Lists files in a directory.directory_path
(string, optional): The path withinassets/
.
exists(asset_path)
: Checks if a file exists.asset_path
(string): The full path to the asset.
load(asset_path)
: Loads the content of an asset.asset_path
(string): The full path to the asset.
local journey_files = turn.assets.list("journeys")
for _, filename in ipairs(journey_files) do
local content = turn.assets.load("journeys/" .. filename)
turn.logger.info("Loaded journey template: " .. filename)
end -
turn.configuration
Manage your app's settings.
get_config()
: Returns a table of the entire configuration.get_config_value(key)
: Returns the value for a specific key.key
(string): The configuration key.
update_config(updates)
: Merges a table of updates into the config.updates
(table): Key-value pairs to update.
set_config(new_config)
: Replaces the entire configuration.new_config
(table): The new configuration table.
local api_key = turn.configuration.get_config_value("api_key")
if not api_key then
turn.logger.error("API key is not configured!")
return false
end
turn.configuration.update_config({ last_synced_at = os.time() }) -
turn.contacts
Find contacts and manage their custom fields.
find(query)
: Finds a contact.query
(table): Key-value pairs to search by (e.g.,{ msisdn = "+27..." }
).
update_contact_details(contact, details)
: Updates a contact's fields.contact
(table): The contact object fromfind()
.details
(table): Key-value pairs of fields to update.
create_contact_field(field_def)
: Creates a new custom field in the schema.field_def
(table): A table withtype
,name
, anddisplay
keys.
local contact, found = turn.contacts.find({ msisdn = "+27820000000" })
if found then
turn.contacts.update_contact_details(contact, { loyalty_id = "LTY-12345" })
end -
turn.google
Authenticate with Google APIs.
get_access_token(service_account_json, scopes)
: Gets an OAuth2 token.service_account_json
(string): The JSON content of the service account file.scopes
(table, optional): A list of Google API scopes.
local ok, token = turn.google.get_access_token(sa_json)
if ok then
-- Use token in turn.http request
end -
turn.http
Make external HTTP requests.
request(options)
: Sends an HTTP request.options
(table): A table withurl
,method
,headers
(table), andbody
(string).
local body, status = turn.http.request({
url = "https://api.example.com/v1/events",
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = turn.json.encode({ message = "Hello" })
}) -
turn.journeys
Programmatically manage Journeys.
create(journey_def)
: Creates a new Journey.journey_def
(table): A table withname
,notebook
, andenabled
.
update(journey_uuid, updates)
: Updates an existing Journey.journey_uuid
(string): The UUID of the journey to update.updates
(table): A table withname
,notebook
, orenabled
.
delete(journey_def)
: Deletes a Journey by name.journey_def
(table): A table with thename
of the journey.
list()
: Returns a list of all Journeys.
local journey, ok = turn.journeys.create({
name = "New User Onboarding",
notebook = turn.assets.load("journeys/onboarding.md"),
enabled = true
}) -
turn.json
Encode and decode JSON data.
encode(data, options)
: Encodes a Lua table into a JSON string.data
(table): The Lua table to encode.options
(table, optional): e.g.,{ indent = true }
.
decode(json_string)
: Decodes a JSON string into a Lua table.json_string
(string): The string to decode.
local my_table = { name = "John Doe", age = 30 }
local json_string = turn.json.encode(my_table) -
turn.leases
Used to resume waiting Journeys. See the asynchronous flow section for a detailed example.
send_input(chat_uuid, input_data)
: Sends data to a paused Journey, resuming it.chat_uuid
(string): The UUID of the chat whose Journey is waiting.input_data
(any): The data to send as the result of theapp()
block.
turn.leases.send_input(chat_uuid, { payment_status = "confirmed" })
-
turn.logger
Write logs that are visible in the Turn UI for debugging.
debug(message)
,info(message)
,warning(message)
,error(message)
message
(string): The log message.
turn.logger.error("Failed to connect to database: " .. err_msg)
-
turn.media
Save binary data (like images or documents) as media that can be reused in messages.
save(media_data)
: Saves binary data as a media item.media_data
(table): A table withdata
(binary string),filename
(string), andcontent_type
(string).
local qr_table, ok = turn.qrcode.generate({ data = "..." })
if ok then
local saved, media_info = turn.media.save(qr_table)
end -
turn.qrcode
Generate QR code images.
generate(options)
: Creates a QR code PNG.options
(table): A table withdata
(string) and optional keys likefilename
,color
,image_data
.
local qr_table, ok = turn.qrcode.generate({
data = "https://www.turn.io/",
color = "#8654CD" -- Turn.io purple!
})
Testing Your App
The app template comes with a complete testing setup using the Busted framework.
- Mock APIs: The
turn.lua
file provides a mock version of the Turn APIs, so you can test your app's logic in isolation. - Writing Tests: Write your tests in the
spec/
directory. You can use spies to assert that your app called the correct Turn APIs. - Running Tests:
# Run all tests from your app's root directory
busted -C .
Packaging for Deployment
When your app is ready, you need to package it into a .zip
file for upload.
- Use the Script: The template's
package.sh
script automates this for you. It will run your tests and then create the ZIP archive in adist/
directory../package.sh --name my_app --version 1.0.0
- Manual Packaging: If you prefer, you can zip the files yourself. Crucially, only include your application's source files (
.lua
) and any assets. Do not include test files or the mockturn.lua
file.
Providing App Documentation in the UI
To ensure a great user experience, you can provide documentation that will appear directly in the Turn UI. Handle the get_app_info_markdown
event and return a Markdown string.
This is the perfect place to explain what your app does, list its features, and provide configuration instructions or API endpoint examples.
elseif event == "get_app_info_markdown" then
return [[
# My Awesome App
This app integrates with the external `XYZ` service.
## Configuration
To use this app, please provide your `XYZ_API_KEY` in the configuration below.
## Webhook Endpoint
Send `POST` requests from your service to the following URL:
`/apps/]] .. app.uuid .. [[/webhook`
]]
end