Skills

August 21, 2026 ยท View on GitHub

Skills are callable capabilities that agents can invoke. This module provides skill addition and management functionality.

Core Concepts

Skill Types

OpenViking supports multiple skill definition formats:

  1. Structured skill data: Dictionary with name, description, content, etc.
  2. SKILL.md files: Markdown files with YAML frontmatter
  3. MCP Tool format: Automatically detected and converted to OpenViking skill format

Skill Storage Structure

Skills are stored under the current user's skills root. The home alias viking://~/skills/ expands to viking://user/{user_id}/skills/ for the authenticated request (the uid-less spelling viking://user/skills/ is no longer accepted):

viking://user/{user_id}/skills/
+-- search-web/
|   +-- .abstract.md      # L0: Brief description
|   +-- .overview.md      # L1: Parameters and usage overview
|   +-- SKILL.md          # L2: Full documentation
|   +-- [auxiliary files]  # Any additional files
+-- calculator/
|   +-- .abstract.md
|   +-- .overview.md
|   +-- SKILL.md
+-- ...

SKILL.md Format

Skills can be defined using SKILL.md files with YAML frontmatter:

---
name: skill-name
description: Brief description of the skill
allowed_tools:
  - Tool1
  - Tool2
tags:
  - tag1
  - tag2
---

# Skill Name

Full skill documentation in Markdown format.

## Parameters
- **param1** (type, required): Description
- **param2** (type, optional): Description

## Usage
When and how to use this skill.

## Examples
Concrete examples of skill invocation.

Required Fields

FieldTypeDescription
namestrSkill name (kebab-case recommended)
descriptionstrBrief description

Optional Fields

FieldTypeDescription
allowed_toolsList[str]Tools this skill can use
tagsList[str]Tags for categorization

MCP Format Automatic Conversion

OpenViking automatically detects and converts MCP tool definitions to skill format.

Detection Rule: A dictionary is treated as MCP format if it contains an inputSchema field.

Conversion Process:

  1. Name is converted to kebab-case
  2. Description is preserved
  3. Parameters are extracted from inputSchema.properties
  4. Required fields are marked from inputSchema.required
  5. Markdown content is generated

Conversion Example:

Input (MCP format):

{
    "name": "search_web",
    "description": "Search the web",
    "inputSchema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "Search query"
            },
            "limit": {
                "type": "integer",
                "description": "Max results"
            }
        },
        "required": ["query"]
    }
}

Output (Skill format):

{
    "name": "search-web",
    "description": "Search the web",
    "content": """---
name: search-web
description: Search the web
---

# search-web

Search the web

## Parameters

- **query** (string) (required): Search query
- **limit** (integer) (optional): Max results

## Usage

This tool wraps the MCP tool `search-web`. Call this when the user needs functionality matching the description above.
"""
}

API Reference

add_skill

Add a skill to the knowledge base.

1. API Implementation Overview

Skills are a special type of resource that define actions or tools agents can perform.

Processing Flow:

  1. Receive skill data or uploaded temporary file
  2. Detect data format (structured data, SKILL.md content, MCP format)
  3. Parse skill definition
  4. Store to the current user's viking://user/{user_id}/skills/ path
  5. If wait=true, wait for vectorization to complete

Code Entry Points:

  • sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_skill - Python SDK entry point
  • openviking_cli/client/http.py:AsyncHTTPClient.add_skill - SDK entry point (HTTP)
  • openviking/server/routers/resources.py:add_skill - HTTP router
  • openviking/service/resource_service.py:ResourceService.add_skill - Core service implementation
  • crates/ov_cli/src/handlers.rs:handle_add_skill - CLI handler

2. Interface and Parameters

Parameters

ParameterTypeRequiredDefaultDescription
dataAnyNo-Inline skill content or structured data. Mutually exclusive with temp_file_id
temp_file_idstrNo-Temporary upload file ID (from temp_upload). Mutually exclusive with data
waitboolNoFalseWait for skill processing to complete
timeoutfloatNoNoneTimeout in seconds, only effective when wait=true
telemetryTelemetryRequestNoFalseWhether to return telemetry data

Additional Notes:

  • Local file handling:

    • Python SDK and CLI accept local SKILL.md files or directories directly. In HTTP mode they automatically upload before calling the server API.
    • Raw HTTP callers should either:
      • Send structured skill data directly in data
      • Send raw SKILL.md content in data
      • First call POST /api/v1/resources/temp_upload to upload a local SKILL.md file/zip directory, then call POST /api/v1/skills with temp_file_id
      • temp_upload defaults to local temporary storage; pass upload_mode=shared only when you explicitly need distributed shared temporary uploads. Python HTTP clients can set upload.mode = "shared" in ovcli.conf; the Rust ov CLI instead uses OPENVIKING_UPLOAD_MODE=shared
    • POST /api/v1/skills does not accept direct host filesystem paths in data.
  • Targeting:

    • Skills are always user-scoped. add_skill does not accept to, parent, or root_uri.
    • Peer-scoped skill roots are not supported; actor peer filtering only applies to peer memories/resources, not peer skills.
    • Use the home alias viking://~/skills/... to address your own skills when listing, reading, deleting, or searching. The uid-less viking://user/skills/... spelling returns an error with a corrective hint.
  • Supported data formats:

    1. Dict (Skill format): Includes name, description, content, etc.
    2. Dict (MCP Tool format): Includes name, description, inputSchema, auto-detected and converted
    3. String (SKILL.md content): Complete SKILL.md content
    4. Path (file or directory): Path to SKILL.md file, or directory containing SKILL.md (auxiliary files included)

3. Usage Examples

HTTP API

POST /api/v1/skills
Content-Type: application/json
# Using inline structured data
curl -X POST http://localhost:1933/api/v1/skills \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "data": {
      "name": "search-web",
      "description": "Search the web for current information",
      "content": "# search-web\n\nSearch the web for current information.\n\n## Parameters\n- **query** (string, required): Search query\n- **limit** (integer, optional): Max results, default 10"
    },
    "wait": true
  }'

# Using inline SKILL.md content
curl -X POST http://localhost:1933/api/v1/skills \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "data": "---\nname: my-skill\ndescription: My custom skill\n---\n\n# My Skill\n\nSkill content here."
  }'

# Using MCP Tool format (auto-detected and converted
curl -X POST http://localhost:1933/api/v1/skills \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "data": {
      "name": "calculator",
      "description": "Perform mathematical calculations",
      "inputSchema": {
        "type": "object",
        "properties": {
          "expression": {
            "type": "string",
            "description": "Mathematical expression to evaluate"
          }
        },
        "required": ["expression"]
      }
    }
  }'

# Using local file (first use temp_upload)
TEMP_FILE_ID=$(
  curl -s -X POST http://localhost:1933/api/v1/resources/temp_upload \
    -H "X-API-Key: your-key" \
    -F "file=@./skills/my-skill.json" \
  | jq -r '.result.temp_file_id'
)

curl -X POST http://localhost:1933/api/v1/skills \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d "{
    \"temp_file_id\": \"$TEMP_FILE_ID\"
  }"

Python SDK

import openviking as ov

client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key")
client.initialize()

# Approach 1: Using structured skill data
skill = {
    "name": "search-web",
    "description": "Search the web for current information",
    "content": """# search-web

Search the web for current information.

## Parameters
- **query** (string, required): Search query
- **limit** (integer, optional): Max results, default 10
"""
}
result = client.add_skill(skill)
print(f"Added: {result['root_uri']}")

# Approach 2: Using MCP Tool format (auto-detected and converted
mcp_tool = {
    "name": "calculator",
    "description": "Perform mathematical calculations",
    "inputSchema": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "Mathematical expression to evaluate"
            }
        },
        "required": ["expression"]
    }
}
result = client.add_skill(mcp_tool)
print(f"Added: {result['uri']}")

# Approach 3: Add from local SKILL.md file
result = client.add_skill("./skills/search-web/SKILL.md")
print(f"Added: {result['uri']}")

# Approach 4: Add from directory containing SKILL.md (auxiliary files included
result = client.add_skill("./skills/code-runner/")
print(f"Added: {result['uri']}")
print(f"Auxiliary files: {result['auxiliary_files']}")

# Wait for processing completion
result = client.add_skill("./skills/my-skill/", wait=True)
client.wait_processed()

TypeScript SDK

await client.addSkill("./my-skill", { wait: true });

Go SDK

result, err := client.AddSkill(ctx, "./skills/my-skill/", &openviking.AddSkillOptions{
    Wait: true,
})
if err != nil {
    return err
}
fmt.Println(result["uri"])

CLI

# Add skill (from file or directory
ov add-skill ./skills/my-skill.json
ov add-skill ./skills/search-web/SKILL.md
ov add-skill ./skills/code-runner/

# Wait for processing completion
ov add-skill ./skills/my-skill/ --wait

# Use JSON output format
ov add-skill ./skills/my-skill/ -o json

Response Examples

HTTP API response (JSON):

{
  "status": "ok",
  "result": {
    "status": "success",
    "root_uri": "viking://user/alice/skills/my-skill",
    "uri": "viking://user/alice/skills/my-skill",
    "name": "my-skill",
    "auxiliary_files": 2,
    "queue_status": {
      "pending": 0,
      "processing": 0,
      "completed": 1
    }
  },
  "telemetry": {
    "operation_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "time": 0.1
}

CLI response (default table format):

Note: Skill is being processed in the background.
Use 'ov wait' to wait for completion, or 'ov observer queue' to check status.
status          success
root_uri        viking://user/alice/skills/my-skill
uri             viking://user/alice/skills/my-skill
name            my-skill
auxiliary_files 2

CLI response (JSON format, using -o json):

{
  "status": "success",
  "root_uri": "viking://user/alice/skills/my-skill",
  "uri": "viking://user/alice/skills/my-skill",
  "name": "my-skill",
  "auxiliary_files": 2
}

Field Description:

FieldTypeDescription
statusstringProcessing status: success or error
root_uristringCanonical final URI of the skill in OpenViking (same as uri)
uristringCanonical final URI of the skill in OpenViking (same as root_uri)
namestringSkill name
auxiliary_filesnumberNumber of auxiliary files included with the skill
queue_statusobject(Optional, only when wait=true) Queue processing status with pending, processing, completed counts

4. Error Handling

Synchronous Processing Errors:

If skill parsing or processing fails synchronously, raw HTTP returns the standard error envelope with a non-2xx HTTP status code:

{
  "status": "error",
  "error": {
    "code": "PROCESSING_ERROR",
    "message": "Skill parse error: invalid skill metadata"
  }
}

The Python HTTP SDK raises the corresponding mapped exception for this response.

Skill Management Operations

The Python HTTP SDK and Go SDK expose dedicated skill management methods: list_skills, find_skills, validate_skill, get_skill, update_skill, and delete_skill in Python; ListSkills, FindSkills, ValidateSkill, GetSkill, UpdateSkill, and DeleteSkill in Go. The general filesystem/content/retrieval methods still work for URI-level access.

List Skills

Python SDK

skills = client.list_skills(node_limit=1000)
for skill in skills["skills"]:
    print(skill["name"])

TypeScript SDK

console.log(await client.listSkills());

Go SDK

skills, err := client.ListSkills(ctx, nil)
_ = skills

HTTP API

curl -X GET "http://localhost:1933/api/v1/skills?node_limit=1000" \
  -H "X-API-Key: your-key"

Read Skill

Python SDK

skill = client.get_skill("search-web", include_content=True, include_files=True)
print(skill["name"])
print(skill.get("content"))

TypeScript SDK

console.log(await client.getSkill("my-skill"));

Go SDK

skill, err := client.GetSkill(ctx, "search-web", &openviking.GetSkillOptions{
    IncludeContent: openviking.Bool(true),
    IncludeFiles:   openviking.Bool(true),
})
_ = skill

HTTP API

curl -X GET "http://localhost:1933/api/v1/skills/search-web?include_content=true&include_files=true" \
  -H "X-API-Key: your-key"

Search Skills

Python SDK

results = client.find_skills("search the internet", limit=5)

for skill in results["skills"]:
    print(skill["name"], skill["score"])

TypeScript SDK

console.log(await client.findSkills("database migration"));

Go SDK

results, err := client.FindSkills(ctx, "search the internet", &openviking.FindSkillsOptions{
    Limit: 5,
})
_ = results

HTTP API

curl -X POST http://localhost:1933/api/v1/skills/find \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "query": "search the internet",
    "limit": 5
  }'

Validate and Update Skills

Python SDK

validated = client.validate_skill({"name": "search-web", "description": "..."})
updated = client.update_skill("search-web", "./skills/search-web", wait=True)

TypeScript SDK

console.log(await client.validateSkill({
  name: "search-web",
  description: "Search the web for current information",
  content: "# search-web\n\nSearch the web for current information.",
}));

Go SDK

validated, err := client.ValidateSkill(ctx, map[string]any{
    "name":        "search-web",
    "description": "...",
}, nil)
updated, err := client.UpdateSkill(ctx, "search-web", "./skills/search-web", &openviking.UpdateSkillOptions{
    Wait: true,
})
_, _ = validated, updated

HTTP API

# Validate skill data
curl -X POST http://localhost:1933/api/v1/skills/validate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{"data": {"name": "search-web", "description": "..."}}'

# Replace an existing skill with new content
curl -X PUT http://localhost:1933/api/v1/skills/search-web \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "data": {
      "name": "search-web",
      "description": "Search the web for current information",
      "content": "# search-web\n\nUpdated instructions."
    },
    "wait": true
  }'

Delete Skills

Python SDK

client.delete_skill("old-skill")

TypeScript SDK

await client.deleteSkill("my-skill");

Go SDK

deleted, err := client.DeleteSkill(ctx, "old-skill")
_ = deleted

HTTP API

curl -X DELETE "http://localhost:1933/api/v1/skills/old-skill" \
  -H "X-API-Key: your-key"

Skill Management Responses

List and search return a skills array and total. Without target_uri, root_uris identifies the private user and shared Agent roots; with a target, the response contains a single root_uri.

{
  "status": "ok",
  "result": {
    "root_uris": [
      "viking://user/default/skills",
      "viking://agent/skills"
    ],
    "skills": [
      {
        "type": "skill",
        "name": "search-web",
        "uri": "viking://user/default/skills/search-web",
        "root_uri": "viking://user/default/skills/search-web",
        "skill_md_uri": "viking://user/default/skills/search-web/SKILL.md",
        "description": "Search the web for current information",
        "tags": [],
        "allowed_tools": [],
        "score": 0.87,
        "match_reason": "semantic",
        "level": 0
      }
    ],
    "total": 1
  }
}

Reading one skill returns the metadata above and conditionally adds abstract, overview, content, files, and source according to level and the include_* parameters.

Validation returns valid, strict, normalized metadata, body_lines, errors, and warnings. Invalid input still uses a successful response envelope with valid=false:

{
  "status": "ok",
  "result": {
    "valid": false,
    "strict": false,
    "name": "search-web",
    "description": "",
    "tags": [],
    "allowed_tools": [],
    "body_lines": 0,
    "errors": [
      {
        "rule": "description_required",
        "message": "description is required",
        "field": "description"
      }
    ],
    "warnings": []
  }
}

A successful update returns the same processing result as add_skill with an additional "action": "update". A successful delete returns:

{
  "status": "ok",
  "result": {
    "name": "old-skill",
    "uri": "viking://user/default/skills/old-skill",
    "root_uri": "viking://user/default/skills/old-skill",
    "estimated_deleted_count": 4,
    "privacy_deleted": false
  }
}

estimated_deleted_count appears only when the filesystem can estimate the number of deleted entries.

Best Practices

Clear Descriptions

# Good - specific and actionable
skill = {
    "name": "search-web",
    "description": "Search the web for current information using Google",
    ...
}

# Less helpful - too vague
skill = {
    "name": "search",
    "description": "Search",
    ...
}

Comprehensive Content

Include in your skill content:

  • Clear parameter descriptions with types
  • When to use the skill
  • Concrete examples
  • Edge cases and limitations

Consistent Naming

Use kebab-case for skill names:

  • search-web (recommended)
  • searchWeb (avoid)
  • search_web (avoid)