---
title: Writing a pack
description: Author a routine pack by hand or from a folder. Required parts, stages, connectors, files, the validation order and the content scan.
sidebar:
  order: 5
---

> Since 0.6.0.

## What it is

A pack is a JSON object with the keys below, in this order. The easiest way to write one is to build the routine as a folder, add a `pack.json` with its metadata, and [export](/templates/export) it.

## Why it exists

Pack rules are strict so a stranger's pack is safe to install. Knowing them up front saves a round of `FAIL` lines.

## How it works

### Top level

| Key | Required | Notes |
|---|---|---|
| `$schema` | no | The schema URL |
| `format` | yes | `routineos.pack.v1` |
| `id` | yes | `^[a-z0-9][a-z0-9-]{0,63}$`, equal to `routine.routine.name` |
| `version` | yes | semver |
| `title` | yes | At most 80 characters |
| `summary` | yes | At most 280 characters |
| `category` | yes | `sales`, `marketing`, `ops`, `support`, `finance`, `recruiting`, `content`, `dev`, `general` |
| `maturity` | yes | `piloted` or `unpiloted` |
| `license` | no | |
| `inspiration` | no | Up to 10 `{source, title, url}`, https only |
| `routine` | yes | `routine.toml` as data |
| `prompt`, `readme` | yes | Text |
| `definition` | when the workspace is enabled | The Desk definition |
| `stages` | yes | 1 to 20; see [Workflow routines](/routine-kinds/workflow) |
| `connectors` | yes | 0 to 20; see [Connectors in packs](/templates/connectors) |
| `files` | no | Up to 40, `{path: text}` |

Every object refuses unknown keys.

### Files

Paths match `^(lib|skills)/[a-z0-9_-]+(/(?!\.\.?/)[A-Za-z0-9_.-]+)*\.(py|md|json|txt)$`, so a `.` or `..` segment is refused. A skill stage needs `skills/<name>/SKILL.md` with a matching frontmatter `name`. A code stage's `command` is one line starting `python3 ` that names `routineos.py` or a `lib/` file in `files`.

### Validation order

1. Size: 512 KiB.
2. JSON, with no duplicate keys.
3. Unknown keys.
4. Types and formats.
5. Routine lint, and `first_run` absent or `report-only`.
6. The definition compiles.
7. Stages and connectors.
8. Files.
9. Content scan: secrets, email addresses, Slack-style ids.
10. A setting named like a recipient (`recipient`, `approver`, `email`, `address`, `cc`, `bcc`, `channel`, `phone`) defaults to an empty list.

## Worked example

To share Hiring Signals, add `hiring-signals/pack.json`:

```json
{
  "format": "routineos.pack.v1", "id": "hiring-signals", "version": "1.0.0",
  "title": "Hiring Signals", "summary": "Watches job boards for commerce rebuild roles.",
  "category": "sales", "maturity": "piloted",
  "stages": [
    {"id": "search", "kind": "model", "title": "Search", "does": "Search each market for matching postings."},
    {"id": "write", "kind": "code", "title": "Write", "does": "Apply settings and write the Desk.",
     "command": "python3 skills/routineos/scripts/routineos.py write --docs snapshot.json --run run.json --out plan"},
    {"id": "intro", "kind": "gate", "title": "Intro note", "does": "Hold any intro note for approval."}
  ],
  "connectors": []
}
```

then `routineos.py pack export hiring-signals --out hiring-signals.pack.json` and `pack validate` it. This exact `pack.json`, on the Hiring Signals folder, exports and prints `ok: hiring-signals 1.0.0 is a valid pack`.

## Common pitfalls

- **An approver's address as a default.** Refused; people add addresses on the Desk.
- **`routine`, `prompt`, `readme`, `definition` or `files` in `pack.json`.** They come from their own files: "'<key>' belongs in its own file, not pack.json".
- **A `lib/` file outside the folder or a symlink.** Export refuses both.

## Related

- [Export a routine](/templates/export)
- [Authoring a template](/templates/authoring)
- [Pack schema](/reference/pack-schema)
