---
summary: Bundle layout, manifest fields, the references directory, semver monotonicity,
  and what the publish endpoint validates.
title: Publish a skill
path: tutorials/publish-a-skill
status: published
---

# Publish a skill

This walks through publishing a real skill — a `pricing-policy` skill that gives an agent the company refund and pricing rules, declares one permission and one secret, and ships a couple of reference documents the model can pull on demand.

Roughly 15 minutes if you have the policy text ready.

## 1. Decide the skill's shape

Before any API calls, pick:

- **Slug.** URL-safe, kebab-case, unique across the workspace. The slug is forever — versions move, the slug doesn't.
- **Visibility.** `private` (only the owning workspace can see it) or `public` (every tenant on the deployment can see it). Default `private`. Public is for shared skills you intend the whole platform to consume.
- **Triggers.** Short words or phrases the LLM can use to recognise the skill applies. Used at search time and shown in the resolved manifest.
- **Permissions and secrets.** What runtime grants does the skill require? Each declared item turns into a pending-grants gate at bind time.

## 2. Lay out the bundle

A bundle is a `.tar.gz` with this shape:

```
SKILL.md
references/
  refund-policy.md
  pricing-tiers.md
```

`SKILL.md` is the only required file. Its YAML frontmatter is the manifest; everything below the closing `---` is the markdown body the LLM reads when it calls `skills.view`. The `references/` directory holds additional files the LLM can pull on demand via `skills.view(slug, path="references/<file>")`.

Lay out the directory:

```
pricing-policy/
├── SKILL.md
└── references/
    ├── refund-policy.md
    └── pricing-tiers.md
```

`SKILL.md` carries the YAML frontmatter (manifest) plus a markdown body:

```yaml
---
name: pricing-policy
version: 0.1.0
description: Acme's pricing tiers, refund windows, and discount policies.
triggers: [pricing, refund, discount, billing]
permissions:
  - scaidrive:read:/policies/
secrets:
  - name: stripe_read_only
    required: true
    description: Stripe API key for fetching invoice metadata.
---

When asked about pricing or refunds, follow these rules:

- Always cite a tier name from `references/pricing-tiers.md`.
- For refund questions, quote the exact policy from `references/refund-policy.md`.
- If the customer cites an invoice id, use the stripe_read_only secret to look it up.
- Never improvise a discount — discounts come strictly from pricing-tiers.md.
```

The `references/` files are plain markdown — the policy text and pricing table the LLM will pull on demand. Then bundle:

```bash
tar -czf pricing-policy-0.1.0.tar.gz -C pricing-policy SKILL.md references
```

## 3. Register the slug

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/skills" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "pricing-policy",
    "visibility": "private",
    "description": "Acme pricing and refund rules."
  }'
```

```python
import httpx, os
res = httpx.post(
    f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaiskills/skills",
    headers={"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"},
    json={
        "slug": "pricing-policy",
        "visibility": "private",
        "description": "Acme pricing and refund rules.",
    },
).raise_for_status()
```

```javascript
await fetch(`${process.env.SCAIGRID_HOST}/v1/modules/scaiskills/skills`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    slug: "pricing-policy",
    visibility: "private",
    description: "Acme pricing and refund rules.",
  }),
});
```

A second publish with a different `version` will not need to repeat this step.

## 4. Publish the first version

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/skills/pricing-policy/versions" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -F "bundle=@pricing-policy-0.1.0.tar.gz"
```

What happens on the server:

1. The validator opens the tarball, reads `SKILL.md`, parses the YAML frontmatter, checks the schema.
2. The slug in the manifest is compared to the URL slug. Mismatch fails validation.
3. The `version` in the manifest is checked against `list_published_semvers(skill_id)` — it must be strictly greater than every previously published version. `0.0.5` after `0.1.0` is rejected.
4. The references directory is checked for path-traversal (`../`, absolute paths) and binary content.
5. A SHA-256 hash of the bundle bytes is computed. If a bundle with the same hash exists for this skill, the put is skipped and the existing storage URI is reused.
6. The version row is written with `status: "published"` and the parsed manifest stored as JSON text.
7. Best-effort: the manifest is indexed into the ScaiMatrix `__scaiskills` collection for semantic search.

## 5. Publish a follow-up version

Iteration: update the body, bump the version, re-tar, re-upload.

```bash
sed -i 's/^version: 0.1.0$/version: 0.2.0/' pricing-policy/SKILL.md
echo "" >> pricing-policy/SKILL.md
echo "**Update 0.2.0:** added VAT handling notes to refund-policy.md." >> pricing-policy/SKILL.md

tar -czf pricing-policy-0.2.0.tar.gz -C pricing-policy SKILL.md references

curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/skills/pricing-policy/versions" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -F "bundle=@pricing-policy-0.2.0.tar.gz"
```

Old bindings keep their pinned or resolved version. New bindings against `latest` or `^0.1` will pick up `0.2.0`.

## 6. Yank if a release is wrong

A yank doesn't break existing bindings — they keep working on the pinned version — but stops new bindings from picking the yanked semver, and `latest`/`^x.y` resolution skips it.

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/skills/pricing-policy/versions/0.2.0/yank" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"
```

After yanking, publish a fixed `0.2.1` rather than republishing under the yanked semver — the version monotonicity check enforces it.

## 7. Confirm via the skill detail endpoint

```bash
curl "$SCAIGRID_HOST/v1/modules/scaiskills/skills/pricing-policy" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"
```

You'll see the slug, the owner workspace, visibility, the description, and the version list with statuses and content hashes. This is the same view the admin UI's catalog page renders.

## Done

You have a versioned skill, a published bundle, and a path to iterate without breaking existing consumers. Next: bind it with grants — the manifest declared one permission and one required secret, so the first binding will sit in `pending_grants` until an admin clears both.
