Skip to content

Building Custom Skills

Scaffold a Skill#

terminal
# Generate a new skill from template
cognest skills create order-lookup

# Output:
# Created skills/order-lookup.ts
# Updated cognest.config.yaml

Skill Structure#

skills/order-lookup.ts
import { defineSkill } from '@cognest/sdk'

export default defineSkill({
  // Required: Identity
  name: 'order-lookup',
  description: 'Look up order status by order ID or customer email',

  // Optional: Version and metadata
  version: '1.0.0',
  author: 'your-team',
  tags: ['ecommerce', 'customer-support'],

  // Optional: Required integrations
  integrations: [],
  permissions: [],

  // Required: Input schema for the Think Engine
  input: {
    order_id: {
      type: 'string',
      description: 'The order ID to look up',
      required: false,
    },
    email: {
      type: 'string',
      description: 'Customer email to find orders for',
      required: false,
    },
  },

  // Required: Execution logic
  async execute(context, input) {
    if (!input.order_id && !input.email) {
      return { text: 'Please provide an order ID or email address.' }
    }

    // Your business logic here
    const order = await lookupOrder(input.order_id ?? input.email)

    if (!order) {
      return { text: 'Order not found. Please check the ID and try again.' }
    }

    return {
      text: `Order #${order.id}: ${order.status}. Shipped ${order.shipped_at}. Tracking: ${order.tracking_number}`,
      data: order,
    }
  },
})

async function lookupOrder(identifier: string) {
  const res = await fetch(`https://api.mystore.com/orders/search?q=${identifier}`)
  return res.json()
}

Using Context#

Accessing Integrations#

skills/with-integrations.ts
async execute(context, input) {
  // Get an integration by name
  const slack = context.integration('slack')
  const gmail = context.integration('gmail')

  // Use integration methods
  await slack.send('#orders', {
    text: `New order lookup: ${input.order_id}`,
  })

  const emails = await gmail.search(`subject:Order ${input.order_id}`)
  return { text: `Found ${emails.length} related emails`, data: emails }
}

Using the Think Engine#

skills/with-think.ts
async execute(context, input) {
  // Call the Think Engine from within a skill
  const summary = await context.think(
    `Summarize this customer's order history in 2 sentences: ${JSON.stringify(input.orders)}`
  )

  // Use structured output
  const analysis = await context.think(
    `Classify this support ticket: ${input.message}`,
    { responseFormat: { type: 'json', schema: { category: 'string', priority: 'number', sentiment: 'string' } } }
  )

  return { text: summary, data: analysis }
}

Persistent State#

skills/with-state.ts
async execute(context, input) {
  // State is scoped per user per integration
  const history = await context.state.get('lookup-history') ?? []

  history.push({
    query: input.order_id,
    timestamp: new Date().toISOString(),
  })

  await context.state.set('lookup-history', history)

  // Access state from the triggering event
  const userId = context.event.from
  context.log.info(`User ${userId} has made ${history.length} lookups`)

  return { text: `Lookup recorded. Total lookups: ${history.length}` }
}

Testing Skills#

skills/order-lookup.test.ts
import { testSkill } from '@cognest/sdk/testing'
import orderLookup from './skills/order-lookup'

describe('order-lookup skill', () => {
  it('should return order status', async () => {
    const result = await testSkill(orderLookup, {
      input: { order_id: 'ORD-12345' },
      // Mock integrations
      integrations: {},
      // Mock state
      state: {},
    })

    expect(result.text).toContain('ORD-12345')
    expect(result.data).toHaveProperty('status')
  })

  it('should handle missing order', async () => {
    const result = await testSkill(orderLookup, {
      input: { order_id: 'INVALID' },
    })

    expect(result.text).toContain('not found')
  })
})
terminal
# Run skill tests
cognest skills test order-lookup

# Test interactively in the CLI
cognest skills run order-lookup --input '{"order_id": "ORD-12345"}'

Scheduled Skills#

skills/daily-digest.ts
import { defineSkill } from '@cognest/sdk'

export default defineSkill({
  name: 'daily-digest',
  description: 'Send a daily summary of activity to Slack',

  // Run on a cron schedule
  schedule: '0 9 * * 1-5', // 9 AM on weekdays

  integrations: ['gmail', 'github', 'slack'],

  async execute(context) {
    const gmail = context.integration('gmail')
    const github = context.integration('github')
    const slack = context.integration('slack')

    const unread = await gmail.getUnread({ since: '24h' })
    const issues = await github.listIssues({ since: '24h', state: 'open' })

    const summary = await context.think(
      `Create a brief daily digest from: ${unread.length} new emails, ${issues.length} new GitHub issues`
    )

    await slack.send('#team', { text: `📊 Daily Digest\n${summary}` })

    return { text: 'Digest sent', data: { emails: unread.length, issues: issues.length } }
  },
})

Best Practices#

  • Write clear descriptions — The Think Engine uses your skill's description and input schema to decide when to invoke it
  • Handle errors gracefully — Return user-friendly error messages instead of throwing raw errors
  • Keep skills focused — Each skill should do one thing well. Compose complex workflows from multiple skills
  • Use structured input — Declare specific input parameters instead of accepting raw text
  • Test with mocks — Use the testing utilities to mock integrations and state