Skip to content

Skills

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:

markdown
---
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):

json
{
    "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):

json
{
    "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. Return a task_id by default for tracking background vectorization

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

Imports return a task_id by default. Query the Task API and search the skill after the task reaches completed.

HTTP API

POST /api/v1/skills
Content-Type: application/json
bash
# 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"
    }
  }'

# 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

python
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(data=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(data=mcp_tool)
print(f"Added: {result['uri']}")

# Approach 3: Add from local SKILL.md file
result = client.add_skill(data="./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(data="./skills/code-runner/")
print(f"Added: {result['uri']}")
print(f"Auxiliary files: {result['auxiliary_files']}")

# Check the status of the previous import task
print(client.get_task(result["task_id"]))

TypeScript SDK

typescript
const result = await client.addSkill("./my-skill");
console.log(result.task_id);

Go SDK

go
result, err := client.AddSkill(ctx, "./skills/my-skill/", nil)
if err != nil {
    return err
}
fmt.Println(result["task_id"])

CLI

ov add-skill and ov skills add share the same options and import flow. The CLI clones Git repositories or GitHub tree directories locally, then packages and uploads the selected skills. The server API still receives inline content or temporary uploads. Supporting files such as references/ and scripts/ are included. Use --list to inspect a collection and --skill to select skills. Batch imports require confirmation unless --yes is set. By default, the command waits for downloading, uploading, server-side parsing, overview generation, and file writes, but not vectorization. --wait additionally waits for vectorization.

bash
# Import one skill from the standalone skills branch; ov skills add also works
ov add-skill https://github.com/volcengine/OpenViking/tree/skills/llm-wiki --wait

# Inspect a local collection, then select skills to import
ov add-skill ./examples/compile/ov-compile-skills --list
ov add-skill ./examples/compile/ov-compile-skills --skill llm-wiki daily-report --yes

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

# Check progress using the task_id returned by submission
ov task status TASK_ID

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

Response Examples

HTTP API response (JSON):

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,
    "task_id": "uuid-xxx"
  },
  "telemetry": {
    "operation_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "time": 0.1
}

CLI response (default table format):

Note: Skill processing may continue in the background.
Use 'ov task status <task_id>' to check progress, or 'ov task list' to see all tasks.
status          success
root_uri        viking://user/alice/skills/my-skill
uri             viking://user/alice/skills/my-skill
name            my-skill
auxiliary_files 2
task_id         uuid-xxx

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

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

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
task_idstringReturned in the default asynchronous mode; query the Task API for the background processing task's final status
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:

json
{
  "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

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

TypeScript SDK

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

Go SDK

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

HTTP API

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

Read Skill

Python SDK

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

TypeScript SDK

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

Go SDK

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

HTTP API

bash
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

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

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

TypeScript SDK

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

Go SDK

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

HTTP API

bash
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

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

TypeScript SDK

typescript
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

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

HTTP API

bash
# 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."
    }
  }'

Delete Skills

Python SDK

python
client.delete_skill(skill_name="old-skill")

TypeScript SDK

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

Go SDK

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

HTTP API

bash
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.

json
{
  "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:

json
{
  "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:

json
{
  "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

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

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

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)

Released under the Apache-2.0 License.