> For the complete documentation index, see [llms.txt](https://docs.khotan.com/llms.txt). A markdown version of any page is available at its `index.md` URL.

# Projects

A **project** is one contract: the piece of work you agreed to deliver, and the spec that says what delivering it means.

Apps, pipelines, and databases are things that run. A project is the intent behind them. It holds the spec, it accumulates revisions as that spec changes, and it is where delivery is tracked.

## Spec

Every project has a **spec**: a tree of nodes with a root goal at the top. Each node carries a title and a markdown body. Read it at one of three altitudes:

```sh
khotan projects spec <projectId>                    # high: the goal
khotan projects spec <projectId> --altitude medium  # adds phases
khotan projects spec <projectId> --altitude low     # adds items and their mirrored issues
```

Altitude is how much of the tree you get, not a different document. Use `high` to see what the project is for, `low` to see the work beneath it.

The spec is versioned. Every write commits a **revision**, and a revision records what changed and why:

```sh
khotan projects revisions <projectId>          # list revisions
khotan projects revision <projectId> <n>       # one revision, with a diff
```

## The repository writes, the dashboard reads

A project's spec is authored **from the repository**, through the CLI or `/api/v1`. The Khotan dashboard shows projects but cannot change them — there is no create button and no spec editor.

This is deliberate. A spec that can be edited in two places drifts, and the version that matters is the one your repository can review, diff, and roll back. Keeping a single write path means the repo is always the truth.

Writes need an API key whose role is `owner`, `admin`, or `developer`. A `member` key can read but not author.

## Changes

Work against a project arrives as an **OpenSpec change**: a directory under `openspec/changes/` holding a proposal, an optional design, a task list, and spec deltas.

```text
openspec
|-- config.yaml                      # names the project this repo delivers
`-- changes
    `-- add-invoice-export
        |-- proposal.md              # why, and what changes
        |-- design.md                # optional; how
        |-- tasks.md                 # the work, as a checklist
        `-- specs
            `-- invoicing
                `-- spec.md          # requirements this change amends
```

Pushing that directory files it as an **issue** under the project:

```sh
khotan change push add-invoice-export
```

A change becomes an issue rather than a project of its own, because a project is the contract and a change is one piece of work beneath it. The proposal becomes the issue description, the design and requirements become attached documents, the task list becomes a checklist, and each amended capability becomes a label.

Pushing a change does not rewrite the project spec. Filing the work and changing the contract are separate acts.

## Metrics

A spec says what the project is for. **Metrics** show whether the thing that was built is working: orders processed, hours saved, exceptions caught. The apps a project spans report them over the API — usually once a day, from the cron job they already run — and the project page leads with them.

An app describes **what a number means, not how to draw it**. Khotan chooses the chart.

| Attribute | Values | What it decides |
|---|---|---|
| `shape` | `series`, `breakdown`, `scatter` | One value per time bucket; one value per category per bucket; or individual x/y observations |
| `unit` | `count`, `percent`, `duration`, `currency:<ISO 4217>`, or a label up to 16 characters | How values are formatted. `percent` is percentage points (`45.2` is 45.2%); `duration` is seconds |
| `goal` | `increase`, `decrease`, `neutral` | Whether a change reads as better or worse. `neutral` carries no judgement |
| `aggregate` | `sum`, `average`, `latest` | How a range becomes one number: add the values, average them, or take the newest bucket |
| `grain` | `day`, `hour` | Bucket size for `series` and `breakdown`. A `scatter` metric has no grain |

A `scatter` metric also names its axes: `xLabel`, `xUnit`, `yLabel`, `yUnit`.

From those, a summed series draws as bars, an averaged or latest series as a line, a breakdown as a donut beside stacked bars (or the top five categories plus "Other" once there are more than five), and a scatter as a plot with labelled axes. Every summary compares the selected range — 7, 30, or 90 days — with the one before it. A range with no points shows no value, never 0.

### Reporting is idempotent

Reports **set** values; they never add to them. A point is identified by its metric and bucket, by bucket and category for a breakdown, or by the `id` you give a scatter point. Sending the same report again changes nothing, so a cron job can retry freely, and sending a different value for a bucket corrects it.

A `day` bucket is a calendar date, `YYYY-MM-DD`, in the app's own business day — Khotan stores it as given, without converting time zones. An `hour` bucket is a timestamp with an offset, stored as its UTC hour. Nothing may be more than a day in the future.

A report is validated in full before anything is written: one bad point rejects the whole report, and the error names the metric key and point index. A metric's `shape` and `grain` are fixed once it exists; to change them, delete the metric and report it again.

### Curation belongs to Khotan

Authors can rename a metric, reorder it, hide it from readers, and mark up to three as **headline** metrics, which lead the Metrics tab and appear on the project's card. A headline shows a range value and its change, so a `scatter` metric cannot be one. Reports never override any of that: a renamed metric keeps its name however the app names it next. The name an app reports is only the default until someone renames it.

A project with at least one visible metric opens on the Metrics tab; otherwise it opens on Delivery. Each metric shows when it was last reported, and is marked **stale** when reports stop — after 48 hours for a `day` metric, 6 hours for an `hour` metric, and 7 days for a `scatter` metric.

### Limits

A project holds up to 100 metrics. A report carries up to 50 metrics and 10,000 points, and a breakdown up to 50 categories per bucket. `series` and `breakdown` points are kept until the metric is deleted; a `scatter` metric keeps its newest 5,000 points.

Reporting, curating, and deleting metrics need an `owner`, `admin`, or `developer` key. Every role can read them. See [Reporting metrics](/api/reporting-metrics/) for a working cron job.

## Documentation

A project's **Docs** tab holds documentation for the people who use what the project delivered: how approvals work, what each role can do, and tutorial videos. Unlike the spec, documentation is written in the dashboard.

### Where pages live

Each page is an ordinary context document in the folder `Project docs/<project-slug>` under **Data → Context**. Only documents placed directly in that folder appear on the Docs tab. Subfolders are ignored, and moving a page out of the folder removes it from the tab.

Owners, admins, and developers see **New page** on the Docs tab. It creates an empty page and opens it in the Context editor. Each page on the tab also has an **Edit** link to the same editor.

### Order

Pages appear in title order, and numbers sort by value, so `2 Approvals` comes before `10 Troubleshooting`. To set an order, start each title with a number.

### Version history

Every save in the Context editor records a revision of that page. Select **History** beside a page to list its revisions and read any earlier one. To restore an earlier revision, use the Context editor.

### Download

**Share → Download PDF** on the Docs tab prints every page into one PDF named `<project name> — Documentation`. Each page shows its revision and the date it was last updated. Each video is replaced by a caption.

### Videos and images

Add media with markdown image syntax. The text in brackets is the caption.

For a video or image uploaded to **Files**, use the file's id with the `khotan-file:` scheme. To copy the id, select the file in **Data → Files** and select **ID** in the details panel.

```md
![How to approve an invoice](khotan-file:<file-id>)
```

For a hosted video, paste its YouTube, Vimeo, or Loom URL:

```md
![Approvals walkthrough](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
```

Files videos stay inside Khotan and are the better choice for anything sensitive. A hosted video sends the reader's IP address to that provider. Other links render as plain links, and raw HTML shows as text.

### Who can read and edit

Every member of the organization can read a project's documentation, whatever their role. Only owners, admins, and developers see **New page** and **Edit**.

The Context editor itself lets any member edit any context document, including documentation pages. The Docs tab never offers a `member` an edit control, but it does not stop a `member` from editing in Context.

## What to read next

- [Reporting metrics](/api/reporting-metrics/) — report KPIs from a cron job
- [OpenSpec](/cli/openspec/) — `change push` and `projects spec commit` in full
- [Projects commands](/cli/commands/projects/) — the generated command reference
- [Projects API](/api/projects/) — the same operations over HTTP
- [Concepts](/basics/concepts/) — organization, workspace, keys
