Cloodot

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

On this page