Skip to main content

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.

TypeWhat it doesUse case
Knowledge SkillsReturns static contentFAQs, policies, reference info
Code SkillsExecutes Lua codeCalculations, 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:

CapabilityCode Skills (UI)App Skills (bundled)
CreationCreated in Turn.io UIBundled in Lua app package
APIs Available5 APIs: HTTP, JSON, Crypto, Encoding, LoggerFull platform API (16+ modules)
Loggingturn.logger (journey logs)turn.logger (app logs)
Contact accessVia context table onlyturn.contacts API available
Journey controlNot availableturn.journeys API available
TemplatesNot availableturn.liquid API available
Memory limit50MBApp-managed
Timeout10 secondsPer-call configurable
StateStateless (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:

  1. Navigate to AI SettingsSkills
  2. Click Create Skill
  3. Select Knowledge as the skill type
  4. Enter a name (snake_case, e.g., clinic_hours)
  5. Enter a description that tells the AI when to use this skill
  6. Enter the content (markdown supported)
  7. 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:

  1. 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.
  2. Parameter schema - The @param annotations define what information the AI Agent should extract from the conversation and pass to your skill.
  3. Return schema - The @return annotation 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 TypeJSON SchemaDescription
stringstringText values
numbernumberDecimal numbers
integerintegerWhole numbers
booleanbooleantrue/false
tableobjectLua tables (key-value)
arrayarraySequential tables
type|niloptionalMarks 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
Security: Use Context, Not Parameters

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:

APIPurpose
turn.httpMake HTTP requests to external APIs
turn.jsonEncode and decode JSON data
turn.cryptoEncryption, hashing, HMAC signatures
turn.encodingBase64, URL encoding, hex conversion
turn.loggerDebug 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))
App Skills have more APIs

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

  1. Navigate to AI SettingsSkills
  2. Click Create Skill
  3. Select Code as the skill type
  4. Enter the skill name and LDoc-annotated Lua code
  5. 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:

  1. Open the AI Agent settings
  2. Navigate to the Skills section
  3. 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:

  1. The AI agent analyzes the user's message and conversation context
  2. If a skill can help fulfill the user's request, the agent calls it
  3. The skill returns its content (Knowledge Skills) or executes and returns results (Code Skills)
  4. 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_policy is better than policy

For Code Skills

  • Use context for identity: Never ask the AI to provide contact data as parameters—use the context table
  • Validate parameters: Treat args values 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)