> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jaasskills.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing a skill

> A complete, real, minimal skill — every file explained

This walks through one full, real skill package — `jaas.demo.personal-notes`, shipped as an example in the `jaas-skills` repo at `examples/skills/personal-notes/`. It's small on purpose: a scratchpad skill that jots a note under a topic and can list what's already there.

<Note>
  Only `manifest.yaml` is strictly required on disk. `schema.json`, `permissions.yaml`, and `dependencies.yaml` each have an unambiguous empty default and are filled in automatically when absent — see [Package layout](/packages/layout). This example includes all five anyway, since a real skill almost always wants a real schema and a documented permission list.
</Note>

## The file tree

```text theme={null}
personal-notes/
├── manifest.yaml
├── SKILL.md
├── schema.json
├── permissions.yaml
├── dependencies.yaml
└── README.md
```

## manifest.yaml

The identity card — id, version, owner, entrypoint, and the runtime this skill targets.

```yaml manifest.yaml theme={null}
apiVersion: v1
id: jaas.demo.personal-notes
name: Personal Notes
version: 1.0.0
description: >-
  Jots down a quick freeform note under a topic and lists what's already
  been noted — a small private scratchpad skill, not meant to be shared.
owner:
  team: jaas-registry-examples
  contact: examples@jaas-registry.local
entrypoint: SKILL.md
category: productivity
tags:
  - demo
  - personal
  - notes
runtime:
  - family: prompt
    versionRange: ">=1.0.0,<2.0.0"
```

`entrypoint: SKILL.md` is the important line for this section — the manifest can name `SKILL.md`, `prompt.md`, `executor.py`, `executor.js`, or `module.wasm` as the entrypoint; whichever it names is packaged alongside the four documents below, but its *contents* are never validated as structured data, since the format is entirely runtime-family-dependent. A `prompt` runtime family (like this one) almost always uses `SKILL.md`.

## SKILL.md

The entrypoint itself — the instructions an agent actually runs. Front-matter `name`/`description` are for documentation only; the manifest's `id`/`name` are the authoritative identity, so the two are allowed to drift (though keeping them aligned avoids confusion).

```markdown SKILL.md theme={null}
---
name: personal-notes
description: Records a short freeform note under a topic, and can list what's already been noted under that topic. Use when asked to jot something down, remember a note for later, or recall previous notes on a topic.
---

# Personal Notes

A small scratchpad skill. Given a `topic` and a `note`:

1. Append the note under that topic (in whatever notes store this
   deployment wires up — this example skill only defines the contract,
   not a specific backend).
2. Report back how many notes now exist under that topic, so the caller
   knows whether this is the first note or one of several.
3. If asked to recall notes for a topic instead of adding one, list them
   in the order they were recorded, most recent last.

Keep entries terse — this is a scratchpad, not a document. Don't
editorialize or summarize the note text; store it as given.
```

## schema.json

The input/output contract, as two JSON Schema documents.

```json schema.json theme={null}
{
  "inputs": {
    "type": "object",
    "properties": {
      "topic": {
        "type": "string",
        "description": "What the note is about, e.g. 'onboarding-todo' or 'ideas'."
      },
      "note": {
        "type": "string",
        "description": "The freeform text to record under that topic."
      }
    },
    "required": ["topic", "note"]
  },
  "outputs": {
    "type": "object",
    "properties": {
      "summary": {
        "type": "string",
        "description": "Confirmation of what was recorded, in one sentence."
      },
      "topicNoteCount": {
        "type": "integer",
        "description": "How many notes now exist under this topic, including this one."
      }
    },
    "required": ["summary"]
  }
}
```

The minimal valid shape, if a skill has no meaningful input/output contract to declare, is `{ "inputs": { "type": "object", "properties": {} }, "outputs": { "type": "object", "properties": {} } }` — this is also exactly what the registry fills in automatically when the file is absent.

## permissions.yaml

A flat list of scopes this skill asks for. No fixed enum — free-form strings, scanned by the PERMISSIONS/SUPPLY\_CHAIN guardrail categories, not validated against a schema (a typo like `fs:writte` is silently accepted as its own custom scope, not rejected).

```yaml permissions.yaml theme={null}
- fs:write
```

## dependencies.yaml

Empty here — this skill depends on nothing else.

```yaml dependencies.yaml theme={null}
[]
```

Non-empty looks like this, and every entry must resolve to an already-published skill at publish time:

```yaml theme={null}
- id: jaas.devtools.git-fundamentals
  versionConstraint: ">=1.0.0,<2.0.0"
```

## Validating before you publish

```bash theme={null}
uv run jaasctl validate examples/skills/personal-notes
```

This runs the same structural checks (§[manifest fields](/packages/manifest), [ID & version policy](/packages/id-version-policy), [dependency policy](/packages/dependencies)) that a publish would, without a guardrails scan and without touching the registry — the fast local loop while you're iterating.

## Checklist

* [ ] `id` follows `vendor.domain.capability`, globally unique
* [ ] `version` is strict SemVer
* [ ] `entrypoint` names a file that actually exists in the package
* [ ] `schema.json` has both `inputs` and `outputs` as top-level keys (if present at all)
* [ ] Every `dependencies.yaml` entry resolves and has no cycle
* [ ] No hardcoded secrets, no `.env`/`config.secrets.json`-style filenames
* [ ] Archive under 50MB

Next: [publish this exact skill through the web UI](/guide/publishing-walkthrough), screenshots included.
