Modern AI agents are capable, but they aren't experts at everything out of the box. You could stuff project conventions and coding standards into a system prompt, but that bloats every conversation and doesn't scale across tasks.
Agent skills solve this problem with a lightweight, open format that packages specialized knowledge and workflows into reusable folders. Originally developed by Anthropic and released as an open standard, the format has since been adopted by many agentic tools and clients — including Junie. In this topic, we'll explore what skills are, how they're organized, and how to write one that captures the conventions of your own project.
What are agent skills?
A skill is simply a folder containing a SKILL.md file, along with any optional supporting materials. Think of it as a self-contained playbook: it tells the agent what a task involves, when to reach for the skill, and how to carry it out step by step.
The key idea behind skills is progressive disclosure. Rather than load everything into the context window at once, agents pull the information in tiers:
At startup, the agent reads only the
nameanddescriptionof each available skill — roughly 100 tokens each. This is just enough to decide when a skill might be relevant.When a user's request matches a description, the agent loads the full
SKILL.mdbody into context and follows its instructions.Only if the task requires them does the agent open supporting files, such as references or scripts.
This layered approach keeps the context window lean.
Skills directory structure
At a minimum, a skill is a directory named after the skill, containing a single SKILL.md file. As tasks grow more complex, you can add optional subdirectories to hold related resources:
skill-name/
├── SKILL.md # Required: metadata + core instructions
├── scripts/ # Optional: executable code (Kotlin, Bash, etc.)
├── references/ # Optional: supplementary docs, schemas, cheatsheets
└── assets/ # Optional: templates, static files, sample dataEach optional directory has a distinct role:
scripts/holds executable code — small command-line utilities the agent can run instead of reasoning through a task manually.references/contains knowledge the agent reads into its context only when needed: API guides, policy documents, worked examples, or data schemas.assets/stores static files used in the output, such as document templates, brand files, or diagrams.
You aren't limited to these three names, but following the convention makes your skill easier for others to understand. Skill folders are typically stored in these locations: .cursor/skills/, .claude/skills/, .codex/skills/, .agent/skills/, and .junie/skills/. The folders can be project- or user-scoped — for a project-specific skill like the one you'll write in this topic, you'll want it committed alongside your code in .junie/skills/, so it travels with the repository and applies to anyone working on it.
A closer look at SKILL.md
The SKILL.md file has two parts: YAML frontmatter enclosed in --- markers, followed by a Markdown body with the instructions. Here's a minimal example, for a skill that helps an agent generate platform-specific implementations in a Kotlin Multiplatform project:
---
name: kmp-actual-implementer description: Generates platform-specific actual implementations for expect declarations in this project. Use when the user adds a new expect declaration, or asks to implement it for Android, iOS, or desktop.
---
# KMP actual implementer
When implementing an `actual` for an `expect` declaration:
1. Locate the `expect` declaration in `commonMain` and read its full signature and KDoc.
2. Implement a matching `actual` in every target source set that doesn't already have one (`androidMain`, `iosMain`, `desktopMain`).
3. Prefer platform APIs already used elsewhere in that source set over adding a new dependency.
4. After implementing, run `./gradlew` allTests to confirm nothing broke.Only two frontmatter fields are required. The name field must contain only lowercase letters, numbers, and hyphens (1–64 characters, no consecutive hyphens). Additionally, it must exactly match the parent folder name — this skill has to live in kmp-actual-implementer/SKILL.md.
The description is the single most important field because it's one of the only things the agent sees before activating the skill. It must state both what the skill does and when to use it. A vague description means the skill never triggers; an overly broad one means it fires when it shouldn't.
You can also have additional fields in the front matter, but these are not required: license, compatibility indicating environment requirements, metadata for arbitrary information, and allowed-tools.
The Markdown body is where you place the actual procedure. This section should be focused; roughly around 500 lines is a good rule of thumb. For lengthy details, you can offload them into references/ files that the agent loads only when required.
Scripts
Sometimes a task is deterministic and better handled by code than by reasoning. Checking that every expect declaration has a matching actual, validating a Gradle version catalog, or generating a file from a template are all things a small script can do reliably every time. That's where the scripts/ directory comes in — and scripts aren't limited to any one language; Kotlin works just as well as Python or Bash.
The recommended pattern is to design scripts as tiny command-line interfaces (CLIs). Your SKILL.md then simply instructs the agent to invoke them:
## Checking expect/actual coverage
To find any expect declaration missing a platform-specific implementation, run the bundled script:
kotlin scripts/check_actuals.main.kts
The script prints a list of `expect` declarations in `commonMain` that don't have a matching `actual` in every target source set. Use this list to know exactly what still needs implementing.This keeps the agent's context clean — it doesn't need to hold the script's internals in memory, only the knowledge that the tool exists and how to call it. It also makes results reproducible, since executing code doesn't drift the way generated reasoning can.
Best practices
Once a SKILL.md is loaded, its contents share the context window with the ongoing conversation and other active skills. Keep it brief. Focus strictly on information the agent wouldn't know without the skill, such as project-specific conventions, rather than general programming knowledge:
<!-- Too verbose — the agent already knows how to write generic error handling -->
## Network requests
When making requests to our internal API, network issues or timeouts can sometimes occur. You should handle errors appropriately and make sure to implement retry logic just in case.
<!-- Better — provides the exact project pattern and concrete edge cases -->
## Network requests
Wrap all internal API calls with the `withRetry` helper from `com.example.app.network`. It retries up to 3 times on `IOException` and matches the pattern already used across `commonMain`.
```kotlin
suspend fun <T> withRetry(times: Int = 3, block: suspend () -> T): T {
repeat(times - 1) {
runCatching { return block() }
}
return block()
}
suspend fun fetchUser(id: String): UserDto =
withRetry { httpClient.get("https://api.internal/users/$id").body() }The description field drives discovery. Test it by running both prompts that should trigger the skill and those that shouldn't, then refine the text until the agent activates it accurately. A description that only says what the skill does, without saying when to reach for it, leaves the agent guessing:
# Too vague — no cue about when this applies, so it rarely triggers
description: Helps with networking code.
# Better — states the task and the triggering conditions
description: Adds retry and error handling to network calls in this project. Use when the user writes a new API call or asks to make an existing one more resilient.When a workflow has multiple valid approaches, provide a clear default path. Offering options without a recommendation leads the agent to waste time guessing and trying different methods before one works — and in a Kotlin Multiplatform project, that guessing gets expensive fast, since there's often a test task per target:
<!-- No default — the agent has to pick, and may try several before one works -->
## Running tests
You can run the suite with `./gradlew` test, a specific target's test task, or your IDE's test runner.
<!-- Better — names the default and reserves the alternative for a specific case -->
## Running tests
Run `./gradlew` allTests from the project root to test every target. Use `./gradlew iosSimulatorArm64Test` only when you need to verify iOS-specific behavior.Keep the high-level steps and tool commands in SKILL.md, but push lengthy materials like API response schemas or design guidelines into the references/ directory. The agent will only pay the token cost to read those files when the task actually requires them.
<!-- Inlined — a 300-line schema sits in context on every activation -->
## API response format
The user profile response has the following fields:
- id (string, UUID) ...
- preferences (object) ...
... 300 more lines ...
<!-- Better — a pointer the agent follows only when it needs the detail -->
## API response format
The response schema is documented in `references/user-profile.schema.json`.
Read it before parsing a response into a Kotlin data class.The most reliable way to write a skill is to complete the task with an agent first. Run through the scenario using the models you plan to deploy — for example, ask Junie to implement an actual for a new expect declaration without any skill in place — note where you have to manually correct its behavior, and add those specific corrections directly into the skill instructions.
Conclusion
Agent skills are an elegant answer to a practical problem: how to give agents specialized expertise without overwhelming their context. At their core, they're just folders with a SKILL.md file, optionally accompanied by scripts/, references/, and assets/. The YAML frontmatter — especially the description — controls discovery, while the Markdown body and supporting files deliver the how-to.
For your project, this is where you'd capture the things you keep having to repeat to Junie or the AI Assistant: which Gradle task to run for tests, how you want expect/actual pairs implemented, or the conventions your Compose Multiplatform UI follows. Writing a couple of focused skills for your own project is a great way to see progressive disclosure in action before you move on to the next topic, where we look at the protocol — MCP — that lets these agents reach beyond your project files in the first place.