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 appversion: A version constraint (e.g.,>=1.0.0,~>2.1,1.0.0)
Dependency behavior:
- If
dependenciesis 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
nameorversion) 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 UIprivate(required):trueto hide from non-admin users,falseto 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: truefor 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 UIfile(required): Path to the markdown file within theassets/directorydescription(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'sassets/directoryfilename(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 agentsfile(required): Path to the skill file within your app package (.luafor code skills,.mdfor 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
nameanddescriptionfields - 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_urlfilter:{{ "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.
Using the Docker SDK (Recommended)
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
.luafile - Includes the
assets/directory withmanifest.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.jsonfile (REQUIRED)- All required Lua modules
- Assets folder with manifest.json and any templates/images
- Any configuration files your app needs
Upload Process
- Go to Turn.io platform
- Navigate to Settings → Apps
- Click "Upload New App"
- Select your ZIP file
- The platform will validate:
- Presence of
assets/manifest.json - Valid JSON structure in manifest
- Required fields (name, version, description)
- Semantic versioning format
- Presence of
- 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:
- Reads the current version from
assets/manifest.json - Computes the next development version (e.g.
1.0.0→1.0.1-dev.1→1.0.1-dev.2) - Uploads the files to the remote instance
- Updates your local
assets/manifest.jsonwith 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.0→1.0.1-dev.1→1.0.1-dev.2 - Promote strips the pre-release tag and applies the bump:
1.0.1-dev.5→1.0.1(patch),1.1.0(minor), or2.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:
- Run
make versions TARGET=stagingto see the deployed versions - Update your local
assets/manifest.jsonversion to match or exceed the current remote version - 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