Adding a Setup Wizard to Your Skill

A skill with a setup wizard gets more installs. Users see a clean, guided configuration flow instead of guessing what values to enter. The platform renders the UI automatically from your manifest — you just declare the fields and steps.

The Basics

You need two things in your skill.json:

  1. A config_schema defining what fields exist
  2. A setup_steps array grouping those fields into screens
{
  "config_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "api_key": {
        "type": "string",
        "title": "API Key",
        "description": "Your service API key (find it in Settings → API)",
        "x-secret": true
      },
      "workspace_id": {
        "type": "string",
        "title": "Workspace ID",
        "description": "The workspace to connect to"
      },
      "notify_on_error": {
        "type": "boolean",
        "title": "Error Notifications",
        "description": "Send a notification when the skill encounters an error",
        "default": true
      }
    },
    "required": ["api_key", "workspace_id"]
  },
  "setup_steps": [
    {
      "step_id": "credentials",
      "title": "Connect Your Account",
      "description": "Enter your API key and workspace ID to get started",
      "fields": ["api_key", "workspace_id"],
      "validation_command": "validate_credentials"
    },
    {
      "step_id": "preferences",
      "title": "Preferences",
      "description": "Configure how the skill behaves",
      "fields": ["notify_on_error"]
    }
  ]
}

The platform renders step 1 first. The user fills in their API key and workspace ID, clicks Next, and the agent validates their credentials before advancing to step 2.

Adding Server-Side Validation

The validation_command field tells the agent to invoke your skill with that operation name before advancing to the next step. This lets you verify credentials, test connections, or check permissions in real time.

Here's a validation handler in Node.js:

const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin });

rl.on('line', (line) => {
  const request = JSON.parse(line);

  if (request.operation === 'validate_credentials') {
    const { api_key, workspace_id } = request.payload;

    // Test the credentials against the external service
    fetch(`https://api.example.com/workspaces/${workspace_id}`, {
      headers: { 'Authorization': `Bearer ${api_key}` }
    })
    .then(res => {
      if (res.ok) {
        process.stdout.write(JSON.stringify({
          status: 'ok',
          result: { message: 'Connected successfully' }
        }) + '\n');
      } else if (res.status === 401) {
        process.stdout.write(JSON.stringify({
          status: 'failed',
          error: 'Invalid API key. Check that your key has workspace access.'
        }) + '\n');
      } else if (res.status === 404) {
        process.stdout.write(JSON.stringify({
          status: 'failed',
          error: 'Workspace not found. Double-check the workspace ID.'
        }) + '\n');
      }
    })
    .catch(err => {
      process.stdout.write(JSON.stringify({
        status: 'failed',
        error: `Connection failed: ${err.message}`
      }) + '\n');
    });
  }
});

The user sees your error message directly in the wizard. Clear, specific error messages ("Invalid API key" vs "Something went wrong") make the difference between a 5-star review and a support ticket.

Field Types in Practice

Secrets (API keys, tokens)

{
  "api_key": {
    "type": "string",
    "title": "API Key",
    "description": "Your service API key",
    "x-secret": true
  }
}

Rendered as a password field. Stored in the encrypted vault. Never shown in logs.

{
  "region": {
    "type": "string",
    "title": "Region",
    "description": "Which data center to connect to",
    "enum": ["us-east", "us-west", "eu-west", "ap-southeast"],
    "default": "us-east"
  }
}

Rendered as a dropdown select. The default pre-selects a value.

Toggles (booleans)

{
  "auto_sync": {
    "type": "boolean",
    "title": "Auto-Sync",
    "description": "Automatically sync data every hour",
    "default": true
  }
}

Rendered as a toggle switch.

Numbers

{
  "max_results": {
    "type": "integer",
    "title": "Max Results",
    "description": "Maximum number of results to return per query",
    "default": 10
  }
}

Rendered as a number input.

Lists (arrays)

{
  "channels": {
    "type": "array",
    "title": "Channels",
    "description": "Slack channels to monitor",
    "items": { "type": "string" },
    "default": ["general"]
  }
}

Rendered as a tag input where users can add/remove values.

Tips for Good Setup Wizards

Keep step 1 minimal. Just credentials. Users want to verify the connection works before configuring preferences.

Write helpful descriptions. Tell users where to find their API key. Link to the service's settings page in the description text.

Use defaults generously. Every field with a sensible default is one less decision for the user.

Validate early. If an API key is wrong, catch it in step 1 — don't let users configure 5 more screens before discovering the connection doesn't work.

Group logically. Credentials in one step, behavior in another, notifications in a third. Don't mix concerns.

Testing Your Wizard

Use the CLI to preview and test:

# Preview how the wizard will render
lsai-cli skill config preview

# Validate your schema
lsai-cli skill config validate

# Test a validation command with sample values
lsai-cli skill config test-validation \
  --step credentials \
  --values '{"api_key": "sk-test", "workspace_id": "ws-123"}'

This runs your validation handler locally so you can verify error messages and success responses before publishing.