Skip to main content

Packaging & Deployment

Packaging for Deployment

When your app is ready, you need to package it into a .zip file for upload. The ZIP file must follow specific structure conventions and include a mandatory manifest.json file.

The manifest.json File (Required)

All apps must include a manifest.json file in the assets/ directory. This file contains your app's metadata:

{
"app": {
"type": "lua",
"name": "my-app",
"title": "My App",
"version": "1.0.0",
"description": "A brief description of what your app does",
"dependencies": [
{
"name": "other-app",
"version": ">=1.0.0"
}
]
},
"contact_fields": [
{
"type": "STRING",
"name": "customer_id",
"display": "Customer ID",
"private": false
}
],
"journeys": [
{
"name": "Welcome Journey",
"file": "journeys/welcome.md",
"description": "Welcomes new users"
}
],
"media_assets": [
{
"asset_path": "images/logo.png",
"filename": "logo.png",
"content_type": "image/png",
"description": "Company logo"
}
],
"skills": [
{
"name": "get_weather",
"file": "my-app/skills/get_weather.lua"
}
]
}

App Metadata (Required)

Required fields:

  • app.type: The type of app (e.g., "lua" for Lua apps)
  • app.name: The name of your app (should match the main Lua file name without extension)
  • app.title: The display title for your app (shown in the Turn UI)
  • app.version: Semantic version string (e.g., 1.0.0, 2.1.3, 1.0.0-beta)
  • app.description: A brief description of your app's functionality

Optional fields:

  • app.dependencies: An array of apps that must be installed for your app to function. Each dependency must specify:
    • name: The exact name of the required app
    • version: A version constraint (e.g., >=1.0.0, ~>2.1, 1.0.0)

Dependency behavior:

  • If dependencies is not specified or is an empty array [], the platform assumes your app has no dependencies and can be installed independently
  • When dependencies are specified, the platform validates that all required apps are installed with compatible versions before allowing installation
  • Invalid dependency entries (missing name or version) are silently ignored

Contact Fields (Optional)

The contact_fields array defines custom fields that your app will add to contacts. These fields are automatically created when the app is installed using turn.manifest.install().

"contact_fields": [
{
"type": "STRING",
"name": "customer_id",
"display": "Customer ID",
"private": false
},
{
"type": "BOOLEAN",
"name": "is_premium",
"display": "Premium Member",
"private": false
},
{
"type": "NUMBER",
"name": "loyalty_points",
"display": "Loyalty Points",
"private": true
}
]

Field properties:

  • type (required): Field type - "STRING", "BOOLEAN", "NUMBER", or "DATE"
  • name (required): Internal field name (lowercase, underscores allowed)
  • display (required): Human-readable label shown in the UI
  • private (required): true to hide from non-admin users, false to show to all

Notes:

  • Fields are created automatically during app installation
  • Fields can be subscribed to for change notifications using turn.app.set_contact_subscriptions()
  • Use private: true for sensitive data like patient records, financial info, or personal identifiers

Journeys (Optional)

The journeys array defines journey templates that your app provides. These are markdown files in your assets/ directory that define conversation flows.

"journeys": [
{
"name": "Welcome Journey",
"file": "journeys/welcome.md",
"description": "Welcomes new users and collects basic information"
},
{
"name": "Payment Flow",
"file": "journeys/payment.md",
"description": "Handles payment processing with Stripe integration"
}
]

Journey properties:

  • name (required): Display name for the journey in the Turn UI
  • file (required): Path to the markdown file within the assets/ directory
  • description (required): Brief description of what the journey does

Notes:

  • Journey files are automatically imported during installation when using turn.manifest.install()
  • Journeys can call back to your app using the app() DSL function
  • Journey markdown files support the full Turn.io journey DSL syntax

Media Assets (Optional)

The media_assets array defines images and other media files included with your app. These are uploaded and made available for use in journeys and messages.

"media_assets": [
{
"asset_path": "images/welcome-banner.jpg",
"filename": "welcome-banner.jpg",
"content_type": "image/jpeg",
"description": "Welcome screen banner image"
},
{
"asset_path": "images/logo.png",
"filename": "logo.png",
"content_type": "image/png",
"description": "Company logo"
},
{
"asset_path": "documents/terms.pdf",
"filename": "terms-of-service.pdf",
"content_type": "application/pdf",
"description": "Terms of service document"
}
]

Media asset properties:

  • asset_path (required): Path to the file within your app's assets/ directory
  • filename (required): Name to use when uploading (can be different from asset_path)
  • content_type (required): MIME type (e.g., "image/jpeg", "image/png", "application/pdf")
  • description (required): Description of the asset's purpose

Supported content types:

  • Images: image/jpeg, image/png, image/gif, image/webp
  • Documents: application/pdf
  • Videos: video/mp4, video/3gpp
  • Audio: audio/mpeg, audio/ogg, audio/aac

Notes:

  • Media files are uploaded to Turn's media storage during installation
  • Uploaded files get unique URLs that can be used in messages and journeys
  • Use turn.assets.read() to access the binary content of assets for uploading

Skills (Optional)

The skills array defines AI agent skills that your app provides. Apps can include both Code Skills (Lua files) and Knowledge Skills (Markdown files).

"skills": [
{
"name": "get_weather",
"file": "my-app/skills/get_weather.lua"
},
{
"name": "lookup_order",
"file": "my-app/skills/lookup_order.lua"
},
{
"name": "return_policy",
"file": "my-app/knowledge/return_policy.md"
}
]

Skill properties:

  • name (required): The skill name used when attaching to AI agents
  • file (required): Path to the skill file within your app package (.lua for code skills, .md for knowledge skills)

Code Skills (.lua):

  • Execute Lua code when called by the AI agent
  • Must include LDoc annotations defining parameters and return types
  • Have access to the full platform API

Knowledge Skills (.md):

  • Return static markdown content when called
  • Use YAML frontmatter with name and description fields
  • Ideal for FAQs, policies, and reference information

Notes:

  • Skills are referenced in AI agents as app:app_name:skill_name
  • See AI Agent Skills for full documentation on writing skills

Without the manifest.json file, your app upload will fail with a :manifest_not_found error.

ZIP File Structure

Your app must follow this structure:

my-app.zip
├── my-app.lua # Main file (required, must match app name)
├── my-app/ # Optional: Modules directory
│ ├── utils.lua
│ ├── handlers.lua
│ └── api/
│ └── client.lua
└── assets/ # Required: Contains manifest and static files
├── manifest.json # REQUIRED: App metadata
├── liquid/ # Liquid templates (used by turn.liquid.render)
│ └── welcome.liquid
├── public/ # Publicly served static files (served via HTTP)
│ ├── svg/
│ │ └── icon.svg
│ ├── css/
│ │ └── styles.css
│ └── images/
│ └── logo.png
├── templates/
│ └── welcome.md
└── images/
└── logo.png

Important: The assets/ directory is now required (not optional) since it must contain the manifest.json file.

assets/public/ — Static Files Served via HTTP

Files in assets/public/ are served directly over HTTP at /apps/:uuid/static/*path, bypassing the Lua runtime. Use this for images, SVGs, stylesheets, fonts, and any other files that need to be loaded by a browser.

  • In Liquid templates, reference them with the static_url filter: {{ "svg/icon.svg" | static_url }}
  • In Lua code, use turn.assets.static_url("svg/icon.svg") to generate the URL

Only files inside assets/public/ are publicly accessible. Other directories under assets/ (like liquid/, templates/, etc.) are not served over HTTP.

If you used the Docker SDK to scaffold your app, packaging is simple:

make build

# This creates my_app-0.0.1.zip (version from manifest.json)

The build command automatically:

  • Includes your main .lua file
  • Includes the assets/ directory with manifest.json
  • Includes any modules in the lib/ directory
  • Excludes test files and development files

Using the Zip-It Script

The traditional template includes a zip-it.sh script that automates packaging:

./zip-it.sh

# The script will remove any old zip files,
# create a new zip file with your latest code

Manual Packaging

If you need more control over packaging:

# Simple app (single file)
zip payment-processor-1.0.0.zip payment-processor.lua

# App with modules
zip -r my-app-1.0.0.zip my-app.lua my-app/

# App with assets
zip -r my-app-1.0.0.zip my-app.lua my-app/ assets/

# Exclude test files
zip -r my-app-1.0.0.zip my-app.lua my-app/ assets/ -x "*/spec/*" "*/turn.lua" "*.rockspec"

# Verify contents
unzip -l my-app-1.0.0.zip

Important Notes

DO NOT include in your ZIP:

  • Test files (spec/ directory)
  • Mock Turn API (turn.lua)
  • Development files (*.rockspec, .git/, etc.)
  • Documentation files (README.md, unless in assets/)

DO include:

  • Main app file (must match app name)
  • assets/manifest.json file (REQUIRED)
  • All required Lua modules
  • Assets folder with manifest.json and any templates/images
  • Any configuration files your app needs

Upload Process

  1. Go to Turn.io platform
  2. Navigate to Settings → Apps
  3. Click "Upload New App"
  4. Select your ZIP file
  5. The platform will validate:
    • Presence of assets/manifest.json
    • Valid JSON structure in manifest
    • Required fields (name, version, description)
    • Semantic versioning format
  6. Upon successful validation, an app definition will be created

Remote Deployment with the SDK

Instead of manually building ZIP files and uploading them through the UI, you can push app files directly to a running Turn instance using the SDK's remote deployment commands. This is the recommended workflow for active development.

Prerequisites

  • Your app was created with the Docker SDK (turn-app new)
  • The app is already installed on the target Turn instance (initial install is done via the UI)
  • You have an API authentication token for the target instance

Setting Up Targets

A target is a named reference to a Turn instance. Configure targets in your app directory:

# Add a target
make target-add NAME=staging URL=https://your-instance.turn.io

# List configured targets
make target-list

# Remove a target
make target-remove NAME=staging

Target configuration is stored locally in .turn-targets.json in your app directory (add this to .gitignore).

Authentication

Set the TURN_APP_TOKEN environment variable with your API token before running push commands:

export TURN_APP_TOKEN=your-api-token

You can also pass it inline with each command:

TURN_APP_TOKEN=your-token make push TARGET=staging

Pushing Files

Push your app files to a remote target:

# Push all files
make push TARGET=staging

# Push only changed files (faster for incremental updates)
make push TARGET=staging CHANGED=--changed

When you push, the SDK:

  1. Reads the current version from assets/manifest.json
  2. Computes the next development version (e.g. 1.0.01.0.1-dev.11.0.1-dev.2)
  3. Uploads the files to the remote instance
  4. Updates your local assets/manifest.json with the new version

The remote instance automatically hot-reloads the app with the new files.

Watch Mode (Auto-Push)

For a live development loop, use watch mode to automatically push changes as you edit files:

make push-watch TARGET=staging

This watches your app directory for file changes and pushes them to the target on each save — similar to make watch for tests but pushes to a remote instance instead.

Promoting to a Release

Development pushes create pre-release versions (e.g. 1.0.1-dev.3). When you're ready to release, promote to a stable version:

# Promote with a patch bump (1.0.1-dev.3 → 1.0.1)
make promote TARGET=staging

# Promote with a minor bump (1.0.1-dev.3 → 1.1.0)
make promote TARGET=staging BUMP=--minor

# Promote with a major bump (1.0.1-dev.3 → 2.0.0)
make promote TARGET=staging BUMP=--major

Promoting pushes all files (not just changed ones) to ensure the release version is a complete snapshot of your app.

Version Management

The SDK manages versions automatically through your assets/manifest.json:

  • Development pushes increment the dev pre-release number: 1.0.01.0.1-dev.11.0.1-dev.2
  • Promote strips the pre-release tag and applies the bump: 1.0.1-dev.51.0.1 (patch), 1.1.0 (minor), or 2.0.0 (major)

You can inspect all versions deployed on a target:

make versions TARGET=staging

Rollback

If you need to revert to a previous version:

make rollback TARGET=staging VERSION=1.0.0

Pulling Journeys from Production

Journeys are often edited in production via the canvas UI. To sync those changes back to your local repo:

make pull TARGET=staging

This downloads the current app archive from the server and overwrites your local assets/journeys/ files. Your git working directory must be clean before pulling.

After pulling, review the changes with your preferred tools (git diff, your editor, etc.) and commit selectively.

Version Conflict Protection

The server rejects pushes with a version older than the current active version (HTTP 409). This can happen if your local assets/manifest.json is out of date — for example, if another developer pushed a newer version.

When this happens:

  1. Run make versions TARGET=staging to see the deployed versions
  2. Update your local assets/manifest.json version to match or exceed the current remote version
  3. Retry the push

Typical Development Workflow

1. Create app:          turn-app new my_app
2. Develop & test: make watch (auto-run tests)
3. Configure target: make target-add NAME=staging URL=https://...
4. Install via UI: Upload initial ZIP through Settings → Apps
5. Push to remote: make push-watch TARGET=staging (live dev loop)
6. Release: make promote TARGET=staging
7. Pull changes: make pull TARGET=staging (sync journey edits)
8. Iterate: Continue pushing dev versions, promote when ready

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