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

> Learn to create custom skills in the OpenWork skills system using YAML frontmatter. Discover how agent capabilities are managed as portable Markdown files.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-09

---

**The OpenWork skills system treats agent capabilities as portable Markdown files with YAML frontmatter, automatically discovering them in [`SKILL.md`](https://github.com/different-ai/openwork/blob/main/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:

- **[`skill-markdown.ts`](https://github.com/different-ai/openwork/blob/main/skill-markdown.ts)** ([`ee/packages/utils/src/skill-markdown.ts`](https://github.com/different-ai/openwork/blob/main/ee/packages/utils/src/skill-markdown.ts)): Low-level parser that extracts frontmatter and body from raw [`SKILL.md`](https://github.com/different-ai/openwork/blob/main/SKILL.md) strings, including the `parseSkillMarkdown()` and `composeSkillMarkdown()` functions.
- **[`frontmatter.ts`](https://github.com/different-ai/openwork/blob/main/frontmatter.ts)** ([`apps/server/src/frontmatter.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/frontmatter.ts)): Generic YAML frontmatter parser (`parseFrontmatter`) and serializer (`buildFrontmatter`).
- **[`skills.ts`](https://github.com/different-ai/openwork/blob/main/skills.ts)** ([`apps/server/src/skills.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/skills.ts)): High-level API that lists, creates, updates, and deletes skills via `listSkills()`, `upsertSkill()`, and related functions.
- **[`validators.ts`](https://github.com/different-ai/openwork/blob/main/validators.ts)** ([`apps/server/src/validators.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/validators.ts)): Enforces naming rules (`validateSkillName`) and description limits (`validateDescription`).

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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/SKILL.md) must begin with a YAML frontmatter block delimited by `---`:

```yaml
---
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/SKILL.md) file inside it. Use the `composeSkillMarkdown()` helper to generate a correctly formatted file programmatically:

```typescript
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:

```typescript
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):

```typescript
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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/apps/server/src/skills.ts) with the same skill name and new content. The function automatically updates the existing [`SKILL.md`](https://github.com/different-ai/openwork/blob/main/SKILL.md) file if present, or creates it if missing, handling frontmatter serialization through `buildFrontmatter()`.