OpenWork Skills System: How to Create Custom Skills with YAML Frontmatter

The OpenWork skills system treats agent capabilities as portable Markdown files with YAML frontmatter, automatically discovering them in SKILL.md files and exposing them through the search_capabilities and execute_capability MCP tools.

The OpenWork platform (different-ai/openwork) enables developers to extend AI agent capabilities using a declarative skills architecture. Each skill is defined as a Markdown file with YAML frontmatter containing metadata like the skill name and description, allowing the system to catalog and invoke custom behaviors across project-local and global directories.

What Is the OpenWork Skills System?

OpenWork treats skills as portable, Markdown-based definitions that agents can invoke. The system consists of several key components working together to parse, validate, and serve skills:

When the desktop app or OpenWork MCP starts, it walks the user's skill directories and builds a catalog of SkillItem objects that represent available capabilities.

How Skills Are Discovered

The discovery process follows four distinct phases, as implemented in apps/server/src/skills.ts:

Locate Skill Roots

The listSkills() function (lines 30-55) walks up from the workspace root to the nearest Git repository, then searches for .opencode/skills and .claude/skills folders. When includeGlobal is enabled, it also scans user-wide directories such as ~/.config/opencode/skills.

Read Each SKILL.md

For every candidate directory, parseSkillEntry() (lines 78-95) reads the file and executes parseFrontmatter from frontmatter.ts to separate the YAML block from the instruction body.

Validate and Construct Catalog Entries

The parsed name and description undergo validation via validateSkillName and validateDescription (lines 90-97). Valid entries become SkillItem objects containing the name, description, file path, scope, and optional trigger phrase.

Expose via MCP

The resulting list is served to the MCP, which presents the skills to agents through the search_capabilities and execute_capability RPCs.

Frontmatter Specification

Every SKILL.md must begin with a YAML frontmatter block delimited by ---:

---
name: my-cool-skill
description: Summarize a web page in 2 sentences.
---

Supported keys include:

  • name (string, required): 1-64 characters, lowercase with hyphens. Must match the directory name. Validated by validateSkillName().
  • description (string, required): Human-readable summary, maximum 1024 characters. Validated by validateDescription().
  • trigger or when (string, optional): A short phrase that suggests the skill when typed. If omitted, the system can infer a trigger from body headings using extractTriggerFromBody().

The parser in skill-markdown.ts (lines 94-123) normalizes values, handling quoted strings and YAML block scalars.

Creating a Custom Skill

Write the Markdown

Create a folder named after your skill and place a SKILL.md file inside it. Use the composeSkillMarkdown() helper to generate a correctly formatted file programmatically:

import { composeSkillMarkdown } from "ee/packages/utils/src/skill-markdown";

const markdown = composeSkillMarkdown(
  "extract-first-paragraph",
  "Return the first paragraph of a web page.",
  `

## When to use

You have a long article and need the opening paragraph only.

## Instructions

1. Fetch the URL.
2. Parse the HTML.
3. Return the text of the first <p> element.
`
);

This helper adds the YAML block, ensures proper escaping, and appends a trailing newline (lines 164-176).

Store the File

Place the file under one of the recognized directories:

  • Project-local: <workspace>/.opencode/skills/<skill-name>/SKILL.md
  • Alternative (Claude): <workspace>/.claude/skills/<skill-name>/SKILL.md

For namespaced layouts (e.g., marketplace plugins), use: <workspace>/.opencode/skills/<plugin>/<skill-name>/SKILL.md. The scanner listSkillsInDir() (lines 120-149) handles both flat and nested layouts.

Validate (Optional)

Run the same validation logic the server uses before deployment:

import { validateSkillName, validateDescription } from "apps/server/src/validators";

validateSkillName("extract-first-paragraph");
validateDescription("Return the first paragraph of a web page.");

Violations throw ApiError with codes like invalid_skill_name or invalid_skill_description.

Register the Skill via API

To save the skill programmatically, use the upsertSkill endpoint (lines 200-228):

import { upsertSkill } from "apps/server/src/skills";

await upsertSkill(workspaceRoot, {
  name: "extract-first-paragraph",
  content: markdown, // the string generated above
});

The function automatically builds or updates the frontmatter to match the payload. After writing, the next discovery run makes the skill instantly searchable.

Full Working Example

This complete example creates a skill that summarizes web pages:

import { composeSkillMarkdown } from "ee/packages/utils/src/skill-markdown";
import { upsertSkill } from "apps/server/src/skills";

async function createSkill(root: string) {
  const md = composeSkillMarkdown(
    "summarize-page",
    "Summarize a web page in 3 bullet points.",
    `

## When to use

You need a quick summary of a long article.

## Instructions

1. Open the URL.
2. Extract the main text.
3. Produce three concise bullet points.
`
  );

  const result = await upsertSkill(root, {
    name: "summarize-page",
    content: md,
  });

  console.log(`Skill ${result.action} at ${result.path}`);
}

// Execute with your workspace root:
createSkill("/path/to/my/project");

Running this script creates or updates <workspace>/.opencode/skills/summarize-page/SKILL.md, immediately making the skill available to OpenWork-compatible agents.

Summary

  • OpenWork skills are Markdown files with YAML frontmatter stored as SKILL.md in specific directory structures.
  • The system discovers skills by scanning .opencode/skills/ and .claude/skills/ directories at both project and global levels.
  • Frontmatter requires name (lowercase, hyphens, 1-64 chars) and description (≤1024 chars), with optional trigger phrases.
  • Use composeSkillMarkdown() to generate valid skill files and upsertSkill() to register them programmatically.
  • Skills are exposed to agents through the search_capabilities and execute_capability MCP tools after validation by validateSkillName() and validateDescription().

Frequently Asked Questions

What file naming convention must I follow for OpenWork skills?

Each skill requires a directory named after the skill (matching the frontmatter name field exactly) containing a file named SKILL.md. The name must be 1-64 characters, lowercase, and use hyphens instead of spaces, as enforced by validateSkillName() in apps/server/src/validators.ts.

Where does OpenWork look for custom skills?

The system scans project-local directories at <workspace>/.opencode/skills/ and <workspace>/.claude/skills/, plus global user directories like ~/.config/opencode/skills/ when includeGlobal is enabled. The listSkills() function in apps/server/src/skills.ts (lines 30-55) orchestrates this discovery.

How do I trigger a skill automatically without manual selection?

Include a trigger or when key in the YAML frontmatter with a short phrase that activates the skill when typed. If omitted, the system attempts to extract a trigger from the first heading in the Markdown body using extractTriggerFromBody().

Can I update an existing skill programmatically?

Yes. Call upsertSkill() from apps/server/src/skills.ts with the same skill name and new content. The function automatically updates the existing SKILL.md file if present, or creates it if missing, handling frontmatter serialization through buildFrontmatter().

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →