Skip to main content
Early Access Program

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.

  1. Download the Template: Get a copy of the lua_app_template directory.

  2. 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
  3. 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
  4. 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

  1. 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
  2. External Webhook Arrives: Later, your payment provider sends a webhook to your app's HTTP endpoint. Your app's http_request handler parses it.

  3. App Resumes the Journey: Inside the http_request handler, you use turn.leases.send_input() with the original chat_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 within assets/.
    • 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 from find().
      • 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 with type, name, and display 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 with url, method, headers (table), and body (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 with name, notebook, and enabled.
    • update(journey_uuid, updates): Updates an existing Journey.
      • journey_uuid (string): The UUID of the journey to update.
      • updates (table): A table with name, notebook, or enabled.
    • delete(journey_def): Deletes a Journey by name.
      • journey_def (table): A table with the name 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 the app() 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 with data (binary string), filename (string), and content_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 with data (string) and optional keys like filename, 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 a dist/ 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 mock turn.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