Skip to main content

Skill Development Guide

How to create, test, and deploy custom skills

πŸ“˜ Skill Development Guide

This guide explains how to create, test, and deploy custom skills.

1. Developing Skills

Handler Function

Every skill must export a handler function with this signature:
export async function handler(input) {
  const { parameters, context, config, secrets } = input

  // Your skill logic here

  return {
    prompt: 'Action completed successfully',
    response: {
      text: "Here's what happened...",
    },
  }
}

Handler Input

  • parameters: Validated inputs based on your JSON Schema.
  • context: Conversation, contact, channel, and organization fields for the current execution.
  • config: Non-sensitive, installation-scoped configuration values.
  • secrets: Sensitive credentials (API keys, tokens). Never log or return these.

Context Fields

  • conversationId: string
  • contactId: string
  • profileName: string
  • organizationId: string
  • organizationName: string
  • organizationDescription: string
  • organizationTimezone: string (IANA timezone)
  • channelType: string (e.g., WHATSAPP, INSTAGRAM, FACEBOOK, WEBCHAT)
  • referenceId: string
  • contactName: string
  • contactEmail: string | null
  • contactPhone: string | null
  • channelName: string
  • channelReference: string | null
Example:
{
  "conversationId": "conv_123",
  "contactId": "ct_456",
  "profileName": "John Doe",
  "organizationId": "org_789",
  "organizationName": "Acme Inc.",
  "organizationDescription": "B2B SaaS for teams",
  "organizationTimezone": "America/New_York",
  "channelType": "WHATSAPP",
  "referenceId": "ref_abc_001",
  "contactName": "John Doe",
  "contactEmail": "john@example.com",
  "contactPhone": "+15551234567",
  "channelName": "Support WhatsApp",
  "channelReference": "15551234567"
}

2. Required Response Format

Your skill handler must return an object that conforms to this schema (aligned with packages/utils/src/action-response.ts):
{
  prompt: string,                              // Required

  // Optional, cites sources or related docs
  knowledgeBaseReferences?: Array<{
    id: string,
    title: string,
    relevance: number
  }>,

  // Optional, message shown to the agent/assistant UI
  message?: string,

  // Optional, rendered content for the end-user
  response?: {
    text?: string,

    // Quick-reply style buttons (strict limits)
    buttons?: Array<{
      label: string,                           // Max 20 characters
      payload: string                          // Max 20 characters
    }>,

    // Rich carousel
    carousel?: Array<{
      title: string,
      subtitle?: string,
      imageUrl?: string,                       // Empty string treated as omitted
      defaultActionUrl?: string,               // Empty string treated as omitted
      buttons?: Array<{
        type: 'web_url' | 'postback',
        title: string,
        url?: string,                          // Use when type === 'web_url' (empty string treated as omitted)
        payload?: string                       // Use when type === 'postback'
      }>
    }>,

    // Send an image as an attachment
    imageUrl?: string                          // Must be HTTPS; empty string treated as omitted
  }
}

Field Details

  • prompt (Required)
    • Type: string
    • Purpose: Brief, human-readable summary of what the skill accomplished
    • Used in conversation logs and AI context
  • knowledgeBaseReferences (Optional)
    • Cite sources or related docs
    • Fields: id, title, relevance (number)
  • message (Optional)
    • A short assistant-facing message, separate from the end-user response
  • response (Optional)
    • Container for rendered content
    • response.text: string
    • response.buttons: interactive buttons
      • label: ≀ 20 characters
      • payload: ≀ 20 characters
    • response.carousel: list of cards
      • Card buttons:
        • type: β€˜web_url’ or β€˜postback’
        • When type is β€˜web_url’, provide url
        • When type is β€˜postback’, provide payload
        • Empty strings for url, imageUrl, and defaultActionUrl are treated as omitted
    • response.imageUrl: HTTPS URL of an image attachment (empty string treated as omitted)

Complete Examples

// Simple Text
return {
  prompt: "Weather retrieved successfully",
  response: {
    text: "Currently 72Β°F and sunny in San Francisco. Perfect weather for a walk!"
  }
}
// Buttons
return {
  prompt: "Support ticket created",
  response: {
    text: "βœ… Ticket #12345 has been created for your billing inquiry.",
    buttons: [
      { label: "View Ticket", payload: "view_12345" },
      { label: "Add Details", payload: "edit_12345" }
    ]
  }
}
// Carousel (with defaultActionUrl)
return {
  prompt: "Found 3 available meeting rooms",
  response: {
    text: "Here are the available rooms for your requested time:",
    carousel: [
      {
        title: "Conference Room A",
        subtitle: "Seats 8 β€’ Projector β€’ Whiteboard",
        imageUrl: "https://office.com/rooms/a.jpg",
        defaultActionUrl: "https://office.com/rooms/a",
        buttons: [
          { type: "postback", title: "Book Room", payload: "book_room_a" }
        ]
      },
      {
        title: "Conference Room B",
        subtitle: "Seats 12 β€’ Video Conf β€’ Catering",
        imageUrl: "https://office.com/rooms/b.jpg",
        buttons: [
          { type: "postback", title: "Book Room", payload: "book_room_b" }
        ]
      }
    ]
  }
}
// Image Attachment (HTTPS required)
return {
  prompt: "Generated product image",
  response: {
    imageUrl: "https://cdn.example.com/images/product-123.png"
  }
}
// With Knowledge Base References
return {
  prompt: "API documentation provided",
  knowledgeBaseReferences: [
    { id: "api-auth-v2", title: "Authentication API v2.0", relevance: 0.92 }
  ],
  response: {
    text: "Include your API key in the Authorization header.",
    buttons: [{ label: "View Examples", payload: "api_examples" }]
  }
}

Error Response Pattern

Return structured responses even on errors:
return {
  prompt: "Unable to complete action",
  response: {
    text: "I couldn't process your request right now. Please try again later.",
    buttons: [
      { label: "Retry", payload: "retry_action" },
      { label: "Get Help", payload: "contact_support" }
    ]
  }
}

3. Parameter Schema

Define inputs using JSON Schema:
{
  "type": "object",
  "properties": {
    "title": { "type": "string", "description": "Meeting title" },
    "attendees": { "type": "array", "items": { "type": "string" }, "description": "Email addresses of attendees" },
    "duration": { "type": "number", "minimum": 15, "maximum": 480, "description": "Meeting duration in minutes" }
  },
  "required": ["title", "duration"]
}
Best Practices:
  • use lowercase property names
  • provide clear descriptions
  • add min/max constraints
  • mark required vs optional
  • keep nesting shallow

4. Configuration & Secrets

Configuration (non-sensitive, per installation)

{
  "api_endpoint": "https://api.example.com/v1",
  "default_owner": "sales-team",
  "sync_frequency": 300
}

Secrets (sensitive; provided via input.secrets)

{
  "crm_api_key": "sk_live_...",
  "webhook_secret": "whsec_...",
  "oauth_token": "ya29...."
}
  • Never log or expose values from secrets.

5. Testing & Validation

Local Testing (Skill Playground)

Use sample parameters and verify your outputs:
// Parameters
{
  "customer_email": "test@example.com",
  "priority": "high"
}

// Expected Response
{
  "prompt": "Support ticket created",
  "response": {
    "text": "Ticket #12345 created for test@example.com"
  }
}
The context includes conversation, organization, contact, and channel fields such as contactName, contactEmail, contactPhone, channelName, and channelReference.

Validation Pipeline

The platform validates:
  • JavaScript syntax & security
  • Parameter schema compliance
  • Response format correctness
  • Performance constraints
  • Best practice adherence
  • Button character limits (label/payload ≀ 20)
  • HTTPS requirement for response.imageUrl
Tip: You can validate your object locally using the exported validateActionResponse() from packages/utils/src/action-response.ts.
  • Updated to include message?: string, response.imageUrl (HTTPS-only), optional carousel defaultActionUrl, and clarified that certain fields treat empty strings as omitted. Removed hard β€œrequired-if-type” constraint for carousel buttons to match current validation.