AI Agent Skills
Skills are capabilities that AI agents can call during conversations. They run natively within Turn.io, providing fast execution without external HTTP calls.
Skill Types
There are two types of skills:
Knowledge Skills return static content when called. Use them to give the AI agent access to reference information like FAQs, policies, product details, or documentation. The AI calls the skill and receives the content to incorporate into its response.
Code Skills execute Lua code with parameters. Use them when the AI needs to perform calculations, call APIs, look up dynamic data, or execute business logic.
| Type | What it does | Use case |
|---|---|---|
| Knowledge Skills | Returns static content | FAQs, policies, reference info |
| Code Skills | Executes Lua code | Calculations, API calls, lookups |
Both types can be created in the Turn.io UI or packaged with a Lua app.
Code Skills vs App Skills
There are two ways to create Code Skills, with different capabilities:
| Capability | Code Skills (UI) | App Skills (bundled) |
|---|---|---|
| Creation | Created in Turn.io UI | Bundled in Lua app package |
| APIs Available | 5 APIs: HTTP, JSON, Crypto, Encoding, Logger | Full platform API (16+ modules) |
| Logging | turn.logger (journey logs) | turn.logger (app logs) |
| Contact access | Via context table only | turn.contacts API available |
| Journey control | Not available | turn.journeys API available |
| Templates | Not available | turn.liquid API available |
| Memory limit | 50MB | App-managed |
| Timeout | 10 seconds | Per-call configurable |
| State | Stateless (fresh each call) | Persistent Lua VM |
When to use Code Skills (UI):
- Simple calculations, data transformations, or validations
- External API lookups that return data to the AI
- Quick prototyping without creating a full app package
When to use App Skills (bundled):
- Skills that need to read or update contact fields
- Operations requiring platform integration (journeys, templates)
- Skills that share configuration or state with other app functionality
Knowledge Skills
Knowledge Skills provide static content that the AI agent can retrieve during conversations. They're ideal for FAQs, policies, product information, or any reference content the AI needs access to.
Creating a Knowledge Skill
In the Turn.io UI:
- Navigate to AI Settings → Skills
- Click Create Skill
- Select Knowledge as the skill type
- Enter a name (snake_case, e.g.,
clinic_hours) - Enter a description that tells the AI when to use this skill
- Enter the content (markdown supported)
- Save the skill
Example Knowledge Skill
Name: return_policy
Description: Returns the company return policy when a customer asks about returns, refunds, or exchanges
Content:
## Return Policy
We accept returns within 30 days of purchase. Items must be:
- Unused and in original packaging
- Accompanied by the receipt or order confirmation
**Refund timeline:** 5-7 business days after we receive the item.
**Exchanges:** Available for the same item in a different size or color.
**Non-returnable items:** Gift cards, personalized items, and final sale items.
To start a return, reply with your order number.
The description is important—it tells the AI agent when to call this skill. Write it clearly so the AI knows to use it when customers ask about returns, refunds, or exchanges.
Code Skills
Code Skills execute Lua code when called. They can accept parameters, access conversation context, call external APIs, and return dynamic results.
Writing Code Skills
A Code Skill is a Lua file with an LDoc header that defines its schema, followed by the implementation code.
Basic Structure
--- Brief description of what the skill does
--
-- @param parameter_name (type): Description of the parameter
-- @return (type): Description of the return value
-- Access parameters from the AI agent
local value = args.parameter_name
-- Access conversation context
local contact_name = context.contact.name
-- Return data to the AI agent
return {
temperature = 22,
condition = "Sunny"
}
LDoc Annotations
The LDoc header at the top of each skill file is read by the Turn.io AI Agent block to understand how to use your skill:
- Skill description - The first line (
---) tells the AI Agent when to call this skill. The AI Agent compares user messages against this description to decide if the skill is relevant. - Parameter schema - The
@paramannotations define what information the AI Agent should extract from the conversation and pass to your skill. - Return schema - The
@returnannotation tells the AI Agent what data to expect back, which it uses to formulate its response.
The description and parameters are automatically extracted and shown to the AI Agent, so write them clearly and specifically.
Description Line
The first comment line becomes the skill's description. The AI Agent block reads this to decide when to call your skill:
--- Get the current weather for a specified city
Write descriptions that clearly explain when the AI should use this skill. When a user sends a message, the AI Agent compares it against all attached skill descriptions. If your skill's description matches the user's intent, the AI Agent will call it.
Parameter Syntax
-- @param name (type): Description
-- @param name (type|nil): Description -- Optional parameter
Type Mapping
| LDoc Type | JSON Schema | Description |
|---|---|---|
string | string | Text values |
number | number | Decimal numbers |
integer | integer | Whole numbers |
boolean | boolean | true/false |
table | object | Lua tables (key-value) |
array | array | Sequential tables |
type|nil | optional | Marks parameter as optional |
Return Values
-- @return (type): Description
Skills typically return a table (object) containing the result data. The AI agent receives this data and uses it to formulate its response.
The args Table
When the AI Agent calls your skill, it extracts parameter values from the conversation based on your @param annotations and passes them in the args table. For example, if a user says "What's the weather in Paris?", the AI Agent extracts "Paris" and passes it as args.city:
Parameters provided by the AI Agent are available in the args table:
local city = args.city -- string parameter
local weight = args.weight -- number parameter
local express = args.express -- boolean parameter (may be nil if optional)
Always validate or provide defaults for parameters:
local city = args.city or "Unknown"
local weight = tonumber(args.weight) or 1
local express = args.express or false
The context Table
The context table provides safe access to conversation data. Always use context for contact, chat, and session data—never ask the AI agent to provide this information as parameters.
-- Contact information
context.contact.name -- Contact's name
context.contact.urn -- Contact's URN (e.g., whatsapp:+1234567890)
context.contact.uuid -- Contact's UUID
context.contact.details -- Custom contact fields (table)
-- Chat information
context.chat.uuid -- Chat UUID
context.chat.title -- Chat title
context.chat.state -- Chat state
context.chat.state_reason -- Reason for current state
context.chat.owner -- Chat owner
context.chat.assigned_to -- Assigned agent
-- Number information
context.number.uuid -- Number UUID
context.number.display_name -- Number display name
context.number.address -- Phone number address
context.number.profile_picture -- Profile picture URL
-- Journey variables
context.vars -- Table of journey variables
Never define parameters for contact identification, authentication tokens, or session data. The AI agent might hallucinate values or be susceptible to prompt injection. Always use the context table for this data—it comes directly from the platform and cannot be manipulated.
Available APIs
Code Skills created in the UI have access to a focused set of APIs optimized for quick, stateless operations:
| API | Purpose |
|---|---|
turn.http | Make HTTP requests to external APIs |
turn.json | Encode and decode JSON data |
turn.crypto | Encryption, hashing, HMAC signatures |
turn.encoding | Base64, URL encoding, hex conversion |
turn.logger | Debug logging (visible in journey logs) |
-- HTTP requests to external APIs
local body, status_code, headers = turn.http.request({
url = "https://api.example.com/data",
method = "GET",
headers = { ["Authorization"] = "Bearer sk_live_xxxxx" }
})
-- JSON encoding/decoding
local data = turn.json.decode(body)
local json_string = turn.json.encode({ result = data.value })
-- Cryptographic operations
local signature = turn.crypto.hmac_sha256_hex("my_secret", json_string)
local hash = turn.crypto.sha256_hex("data to hash")
-- Encoding utilities
local encoded = turn.encoding.base64_encode("binary data")
local url_safe = turn.encoding.url_encode("hello world")
-- Logging (visible in journey logs UI)
turn.logger.debug("Processing request...")
turn.logger.info("Fetched data for: " .. args.account_id)
turn.logger.warning("Rate limit approaching")
turn.logger.error("API call failed: " .. tostring(status_code))
Skills bundled with Lua apps have access to additional APIs including turn.contacts, turn.journeys, turn.liquid, and more. See the API Reference for the complete list.
Creating Code Skills via UI
- Navigate to AI Settings → Skills
- Click Create Skill
- Select Code as the skill type
- Enter the skill name and LDoc-annotated Lua code
- Save the skill
Common Patterns
Here are practical examples showing common Code Skill patterns using the available APIs.
HTTP API Call
Fetch data from an external API with authentication:
--- Get account balance from payment API
-- @param account_id (string): The account ID to look up
-- @return (object): Account balance information
local body, status_code = turn.http.request({
url = "https://api.example.com/accounts/" .. args.account_id .. "/balance",
method = "GET",
headers = {
["Authorization"] = "Bearer sk_live_xxxxx", -- Your API key
["Content-Type"] = "application/json"
}
})
if status_code ~= 200 then
return { error = "Failed to fetch balance", status = status_code }
end
local data = turn.json.decode(body)
return {
account_id = args.account_id,
balance = data.balance,
currency = data.currency
}
Order Calculation
Calculate totals from simple parameters:
--- Calculate order total with tax
-- @param product_name (string): Name of the product
-- @param quantity (integer): Number of items
-- @return (object): Order summary with total
-- Product catalog (prices should come from your backend, not user input)
local prices = {
["Widget"] = 9.99,
["Gadget"] = 24.99,
["Gizmo"] = 14.99
}
local product = args.product_name or "Widget"
local qty = tonumber(args.quantity) or 1
local price = prices[product] or 9.99
local subtotal = qty * price
local tax = subtotal * 0.1 -- 10% tax
local total = subtotal + tax
return {
product = product,
quantity = qty,
unit_price = string.format("%.2f", price),
subtotal = string.format("%.2f", subtotal),
tax = string.format("%.2f", tax),
total = string.format("%.2f", total)
}
Error Handling
Handle API errors based on response status:
--- Call external API with error handling
-- @param endpoint (string): API endpoint to call
-- @return (object): Result or error information
local body, status_code = turn.http.request({
url = "https://api.example.com/" .. args.endpoint,
method = "GET",
timeout = 5000
})
if status_code >= 400 then
return {
success = false,
error = "API error",
status = status_code
}
end
return {
success = true,
data = turn.json.decode(body)
}
Debugging with Logging
Use turn.logger to trace execution and debug issues. Logs appear in the journey logs UI:
--- Look up product inventory
-- @param product_id (string): Product ID to check
-- @return (object): Inventory information
turn.logger.info("Looking up inventory for: " .. args.product_id)
local body, status_code = turn.http.request({
url = "https://api.inventory.com/products/" .. args.product_id,
method = "GET",
headers = { ["Authorization"] = "Bearer sk_live_xxxxx" }
})
turn.logger.debug("API response status: " .. tostring(status_code))
if status_code ~= 200 then
turn.logger.error("Inventory lookup failed: " .. tostring(status_code))
return { error = "Product not found", status = status_code }
end
local data = turn.json.decode(body)
turn.logger.info("Found " .. tostring(data.quantity) .. " units in stock")
return {
product_id = args.product_id,
in_stock = data.quantity > 0,
quantity = data.quantity
}
Calculations and Formatting
Perform calculations and return formatted results:
--- Calculate loan payment
-- @param principal (number): Loan principal amount
-- @param rate (number): Annual interest rate (e.g., 5.5 for 5.5%)
-- @param years (integer): Loan term in years
-- @return (object): Monthly payment and total cost
local principal = tonumber(args.principal) or 0
local annual_rate = tonumber(args.rate) or 0
local years = tonumber(args.years) or 1
-- Convert annual rate to monthly
local monthly_rate = (annual_rate / 100) / 12
local num_payments = years * 12
-- Calculate monthly payment using amortization formula
local monthly_payment
if monthly_rate == 0 then
monthly_payment = principal / num_payments
else
monthly_payment = principal *
(monthly_rate * math.pow(1 + monthly_rate, num_payments)) /
(math.pow(1 + monthly_rate, num_payments) - 1)
end
local total_paid = monthly_payment * num_payments
local total_interest = total_paid - principal
return {
monthly_payment = string.format("$%.2f", monthly_payment),
total_paid = string.format("$%.2f", total_paid),
total_interest = string.format("$%.2f", total_interest),
num_payments = num_payments
}
POST Request with JSON Body
Send data to an external API:
--- Submit a support ticket to external system
-- @param subject (string): Ticket subject
-- @param message (string): Ticket description
-- @return (object): Created ticket information
local payload = turn.json.encode({
subject = args.subject,
message = args.message,
customer_name = context.contact.name,
customer_phone = context.contact.urn,
source = "whatsapp"
})
local body, status_code = turn.http.request({
url = "https://api.helpdesk.com/tickets",
method = "POST",
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer sk_live_xxxxx" -- Your API key
},
body = payload
})
if status_code ~= 201 then
return { error = "Failed to create ticket", status = status_code }
end
local ticket = turn.json.decode(body)
return {
success = true,
ticket_id = ticket.id,
ticket_url = ticket.url
}
API with Signature Authentication
Call an API that requires HMAC signature authentication:
--- Check account status with partner API
-- @param account_id (string): Account to check
-- @return (object): Account status information
local api_key = "pk_xxxxx" -- Your API key
local api_secret = "sk_xxxxx" -- Your API secret
local timestamp = tostring(os.time())
-- Build the string to sign
local request_path = "/accounts/" .. args.account_id .. "/status"
local string_to_sign = "GET" .. request_path .. timestamp
-- Generate HMAC signature
local signature = turn.crypto.hmac_sha256_hex(api_secret, string_to_sign)
local body, status_code = turn.http.request({
url = "https://api.partner.com" .. request_path,
method = "GET",
headers = {
["X-Api-Key"] = api_key,
["X-Timestamp"] = timestamp,
["X-Signature"] = signature
}
})
if status_code ~= 200 then
return { error = "Failed to check account", status = status_code }
end
local data = turn.json.decode(body)
return {
account_id = args.account_id,
status = data.status,
balance = data.balance,
last_activity = data.last_activity
}
Lookup with Fallback
Fetch data from an external system with graceful fallback:
--- Get customer info from CRM
-- @param customer_id (string): Customer ID to look up
-- @return (object): Customer information or defaults
local body, status_code = turn.http.request({
url = "https://api.crm.com/customers/" .. args.customer_id,
method = "GET",
headers = {
["Authorization"] = "Bearer sk_live_xxxxx" -- Your API key
}
})
-- Return defaults if customer not found
if status_code == 404 then
return {
found = false,
customer_id = args.customer_id,
name = context.contact.name or "Valued Customer",
tier = "standard",
discount = 0
}
end
if status_code ~= 200 then
return { error = "CRM lookup failed", status = status_code }
end
local customer = turn.json.decode(body)
return {
found = true,
customer_id = customer.id,
name = customer.name,
tier = customer.loyalty_tier,
discount = customer.discount_percent,
lifetime_value = customer.total_spent
}
Limitations
Code Skills have the following constraints:
- Timeout: 10 seconds maximum execution time
- Memory: 50MB maximum memory usage
- Stateless: Each execution starts fresh (no persistent state between calls)
- APIs: Limited to HTTP, JSON, Crypto, Encoding, Logger (no Contacts, Journeys)
For complex use cases requiring persistent state or full platform API access, use App Skills instead.
App Skills
App Skills are bundled with Lua apps and declared in the app's manifest.json.
Manifest Declaration
{
"app": {
"type": "lua",
"name": "my_app",
"title": "My App",
"version": "1.0.0",
"description": "App with AI skills"
},
"skills": [
{"name": "get_weather", "file": "my_app/skills/get_weather.lua"},
{"name": "lookup_order", "file": "my_app/skills/lookup_order.lua"}
]
}
File Organization
my_app.zip
├── my_app.lua # Main app file
├── my_app/
│ └── skills/
│ ├── get_weather.lua
│ └── lookup_order.lua
└── assets/
└── manifest.json
Reference Format
App Skills are referenced using the format: app:app_name:skill_name
For example: app:my_app:get_weather
Complete Examples
Weather Lookup (Basic)
A simple skill that takes a city name and returns weather data:
--- Get the current weather for a specified city
--
-- @param city (string): The name of the city to get weather for
-- @return (object): Weather information including temperature and conditions
local city_name = args.city or "Unknown"
local requested_by = context.contact.name or "Unknown"
-- In production, call a real weather API here
local conditions = {"Sunny", "Cloudy", "Rainy", "Partly Cloudy", "Windy"}
return {
city = city_name,
temperature = math.random(10, 35),
condition = conditions[math.random(1, #conditions)],
humidity = math.random(30, 90) .. "%",
requested_by = requested_by
}
Shipping Calculator (Optional Parameters)
A skill demonstrating optional boolean parameters:
--- Calculate shipping cost for a package
--
-- @param weight (number): Package weight in kilograms
-- @param destination (string): Destination type - "domestic" or "international"
-- @param express (boolean|nil): Whether to use express shipping (optional)
-- @return (object): Shipping quote with cost and estimated delivery time
local pkg_weight = tonumber(args.weight) or 1
local dest = args.destination or "domestic"
local is_express = args.express or false
local customer_name = context.contact.name or "Customer"
-- Calculate shipping cost
local base_cost = 5.00
local weight_cost = pkg_weight * 2.50
local destination_multiplier = (dest == "international") and 3.0 or 1.0
local express_multiplier = is_express and 2.0 or 1.0
local total = (base_cost + weight_cost) * destination_multiplier * express_multiplier
return {
customer = customer_name,
weight_kg = pkg_weight,
destination = dest,
express = is_express,
cost = string.format("$%.2f", total),
estimated_days = is_express and "1-2 business days" or "5-7 business days"
}
Order Lookup (Context Usage)
A skill that uses context to personalize the response:
--- Look up the status of an order by its ID
--
-- @param order_id (string): The order ID to look up
-- @return (object): Order details including status and tracking
local id = args.order_id or "unknown"
local customer_name = context.contact.name or "Customer"
-- In production, query your order database here
local statuses = {"Processing", "Shipped", "Out for Delivery", "Delivered"}
return {
order_id = id,
customer = customer_name,
status = statuses[math.random(1, #statuses)],
last_updated = os.date("%Y-%m-%d %H:%M"),
items = {
{ name = "Widget A", quantity = 2, price = "$19.99" },
{ name = "Gadget B", quantity = 1, price = "$49.99" }
},
total = "$89.97",
tracking_number = "TRK" .. math.random(100000, 999999)
}
Using Skills in AI Agents
Attaching Skills
When configuring an AI Agent card in a journey:
- Open the AI Agent settings
- Navigate to the Skills section
- Select skills to attach:
- Knowledge Skills and Code Skills appear by name
- App Skills appear as
app:app_name:skill_name
Runtime Behavior
During a conversation:
- The AI agent analyzes the user's message and conversation context
- If a skill can help fulfill the user's request, the agent calls it
- The skill returns its content (Knowledge Skills) or executes and returns results (Code Skills)
- The agent incorporates the results into its response to the user
The AI agent decides when to use skills based on the skill descriptions. Write clear descriptions so the AI knows when each skill is relevant.
Best Practices
For All Skills
- Write clear descriptions: The description tells the AI when to use the skill—be specific
- Keep skills focused: Each skill should do one thing well
- Use descriptive names:
get_return_policyis better thanpolicy
For Code Skills
- Use context for identity: Never ask the AI to provide contact data as parameters—use the
contexttable - Validate parameters: Treat
argsvalues as untrusted input - Keep execution fast: Skills should complete quickly to maintain conversation flow
- Handle errors gracefully: Return meaningful error information rather than failing silently
For Knowledge Skills
- Keep content current: Review and update knowledge content regularly
- Structure for clarity: Use markdown formatting to make content easy to read
- Be comprehensive: Include all relevant information the AI might need
Troubleshooting
Skill Not Being Called
- Check that the skill is attached to the AI Agent card
- Verify the description clearly explains when to use the skill
- Review the AI agent's system prompt for conflicting instructions
Code Skill Issues
Parameter parsing errors:
- Ensure LDoc annotations use correct syntax:
-- @param name (type): Description - Verify types match what the AI agent is providing
Context data missing:
- Check that you're accessing the correct context path (e.g.,
context.contact.name) - Remember that some context fields may be nil
Unexpected results:
- Return intermediate values in the result object for debugging
- Verify parameter values before using them
- Use
turn.logger.info()to trace execution (logs appear in journey logs)