Skill Best Practices
Design patterns and optimization guidelines
Follow these best practices to create robust, efficient, and maintainable skills.
Code Quality
Error Handling
Always wrap logic in try-catch:
async function handler(input) {
try {
// Your logic
return { message: "Success" }
} catch (error) {
// Return error without throwing
return {
message: `Error: ${error.message}`,
status: "error"
}
}
}Input Validation
Validate all parameters before processing:
async function handler(input) {
const { parameters } = input
// Check required fields
if (!parameters.orderId) {
return {
message: "Error: Order ID is required",
status: "error"
}
}
// Validate format
if (!isValidOrderFormat(parameters.orderId)) {
return {
message: "Error: Invalid order ID format",
status: "error"
}
}
// Continue with logic...
}Logging
Use console for debugging. Logs are captured and returned:
async function handler(input) {
const { parameters } = input
console.log(`Processing order: ${parameters.orderId}`)
try {
const result = await fetchOrder(parameters.orderId)
console.info(`Order found: ${result.status}`)
return { message: "Success", ...result }
} catch (error) {
console.error(`Failed to fetch order: ${error.message}`)
return { message: "Error", status: "error" }
}
}Performance
Optimize External Calls
// ❌ Bad: Unnecessary API calls
async function handler(input) {
// First call
const customer = await fetch(`/api/customers/${id}`)
// Second call
const history = await fetch(`/api/customers/${id}/history`)
// Could be batched
}
// ✅ Good: Batch requests
async function handler(input) {
const [customer, history] = await Promise.all([
fetch(`/api/customers/${id}`),
fetch(`/api/customers/${id}/history`)
])
}Timeout Configuration
Set reasonable timeouts for external calls:
async function handler(input) {
const { config } = input
const timeout = config.timeout || 5000 // 5 second default
try {
const response = await fetch(url, { signal: AbortSignal.timeout(timeout) })
} catch (error) {
if (error.name === 'AbortError') {
return { message: "Request timeout", status: "error" }
}
}
}Cache When Possible
For skills that call the same API repeatedly:
const cache = new Map()
async function handler(input) {
const { parameters } = input
// Check cache
const cacheKey = `order_${parameters.orderId}`
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
// Fetch if not cached
const result = await fetchOrder(parameters.orderId)
// Cache for 5 minutes
cache.set(cacheKey, result)
setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000)
return result
}Design Patterns
Check Before Act
Always verify state before performing actions:
async function handler(input) {
const { parameters } = input
// Check current status
const order = await getOrder(parameters.orderId)
if (order.status === 'CANCELLED') {
return {
message: "Cannot process cancelled order",
status: "error"
}
}
// Proceed with action
return processOrder(order)
}Idempotency
Design skills to be safe if called multiple times:
async function handler(input) {
const { parameters } = input
// Check if already processed
const existingPayment = await getPayment(parameters.transactionId)
if (existingPayment) {
return {
message: "Payment already processed",
transactionId: existingPayment.id,
status: "duplicate"
}
}
// Only process once
return createPayment(parameters)
}Progressive Information Gathering
For interactive skills, collect info gradually:
async function handler(input) {
const { parameters } = input
// Check what we have
if (!parameters.orderId) {
return {
message: "I need the order ID first",
status: "needs_info"
}
}
if (!parameters.action) {
return {
message: "What would you like to do?",
buttons: [
{ label: "Track", payload: "TRACK" },
{ label: "Return", payload: "RETURN" }
]
}
}
// Now we have everything
return performAction(parameters)
}Security
Configuration Values
Use sensitive configuration for secrets:
// In SkillSet definition
{
key: "apiKey",
type: "SECRET", // Hides value in UI
isSensitive: true, // Never logs value
required: true
}Input Sanitization
Validate and sanitize user input:
async function handler(input) {
const { parameters } = input
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(parameters.email)) {
return { message: "Invalid email format" }
}
// Limit string length
if (parameters.notes?.length > 1000) {
return { message: "Notes too long (max 1000 chars)" }
}
// Whitelist values
const allowedStatuses = ["pending", "processing", "completed"]
if (!allowedStatuses.includes(parameters.status)) {
return { message: "Invalid status" }
}
}Rate Limiting
Prevent abuse of your skill:
const rateLimits = new Map()
async function handler(input) {
const { context } = input
const key = `${context.organizationId}_${context.conversationId}`
// Check rate limit
const now = Date.now()
const lastCall = rateLimits.get(key)
if (lastCall && now - lastCall < 1000) { // 1 second minimum
return { message: "Too many requests, please wait" }
}
rateLimits.set(key, now)
// Proceed...
}Testing
Unit Testing Pattern
// skill.js
async function handler(input) {
if (!input.parameters.amount) {
throw new Error('Amount required')
}
return { amount: input.parameters.amount }
}
// skill.test.js
async function testValidAmount() {
const result = await handler({
parameters: { amount: 100 },
config: {}
})
console.assert(result.amount === 100)
}
async function testMissingAmount() {
try {
await handler({
parameters: {},
config: {}
})
console.error('Should have thrown')
} catch (e) {
console.assert(e.message === 'Amount required')
}
}Integration Testing
Test with real external services in a staging environment:
// Only run in staging
const isStaging = process.env.ENVIRONMENT === 'staging'
async function handler(input) {
if (!isStaging && input.parameters.testMode) {
return { message: "Test mode only available in staging" }
}
// Real execution
}Documentation
Clear Prompts
Write prompts that help the AI understand when to use the skill:
// ❌ Too vague
prompt: "Check status"
// ✅ Clear and specific
prompt: `Use this skill when a customer asks about their order status,
delivery date, or tracking information.
It takes an order ID and returns current status
(pending, processing, shipped, delivered, or cancelled).`Parameter Descriptions
Document each parameter clearly:
{
"parameters": {
"type": "object",
"properties": {
"orderId": {
"type": "string",
"description": "The order ID (e.g., 'ORD-12345'). Found in order confirmation email."
},
"includeHistory": {
"type": "boolean",
"description": "If true, returns full order history and previous statuses"
}
}
}
}Monitoring
Return Diagnostic Information
Include information that helps diagnose issues:
async function handler(input) {
try {
const response = await fetch(url)
if (!response.ok) {
return {
message: `Request failed: ${response.status}`,
statusCode: response.status,
timestamp: new Date().toISOString(),
status: "error"
}
}
return { message: "Success", ...data }
} catch (error) {
console.error('Skill error:', error.stack)
return {
message: `Error: ${error.message}`,
errorType: error.constructor.name,
status: "error"
}
}
}Meaningful Status Values
Return status to help track issues:
return {
message: "...",
status: "success", // Operation succeeded
// or
status: "error", // System error occurred
// or
status: "invalid_input", // User provided bad input
// or
status: "not_found", // Resource doesn't exist
}Common Mistakes to Avoid
❌ Don't Throw Errors
The skill should never throw, always return errors:
// ❌ Bad
async function handler(input) {
throw new Error("Invalid input") // AI won't know how to handle
}
// ✅ Good
async function handler(input) {
return { message: "Invalid input", status: "error" }
}❌ Don't Use Blocking Operations
All I/O should be async:
// ❌ Bad
const data = fs.readFileSync('/file.txt') // Blocks execution
// ✅ Good
const data = await readFile('/file.txt') // Non-blocking❌ Don't Log Sensitive Data
// ❌ Bad
console.log(`API Key: ${config.apiKey}`) // Exposes secret
// ✅ Good
console.log('Authenticating with API...') // No secrets❌ Don't Assume Parameters Exist
// ❌ Bad
const id = input.parameters.orderId.toUpperCase() // May crash
// ✅ Good
const id = input.parameters.orderId?.toUpperCase() || 'unknown'Checklist
Before deploying a skill:
- Handler function is defined and async
- All parameters are validated
- Errors are caught and returned (not thrown)
- External calls have timeouts
- Sensitive values use SECRET config type
- Logs don't expose secrets
- Prompt clearly describes when to use
- Response schema matches actual output
- Code is tested with various inputs
- No dangerous APIs (eval, fs, etc.)
- Error messages are user-friendly
- Status values are meaningful