# Agent manifests (/docs/build/agent-manifests)



```sh
npx nylorun init agents/support
npx nylorun check agents/support
npx nylorun apply agents/support --agent
npx nylorun chat support --message "Why was I charged twice in March?"
```

```markdown title="agents/support/AGENT.md"
---
name: support
description: Answers billing questions in two sentences or fewer.
---
You answer questions about invoices and refunds. If a question is not about
billing, say so and stop.
```

An agent manifest is written as files: `AGENT.md` with its name,
description, and instructions, and optional JSON files for its runtime, MCP
servers, HTTP tools, and, for a flow, `flow.json`. `nylorun check` compiles the
files offline into the manifest the Runtime saves (see
[Saving manifests](/docs/run/agent-manifests)). `nylorun apply` sends them to
the Runtime, which compiles them again with the same compiler, saves the
manifest's next version, and keeps the files byte for byte as that version's
source. `--agent` then makes the
[Agent](/docs/run/agents) `support` from the manifest, and `nylorun chat`
talks to it. Sessions run the saved manifest, never the files.

<Callout title="Versions">
  Manifests written as files need `nylorun` 0.20.0-beta, `@nylorun/agents` 0.28.0-beta, and
  Runtime 0.32.0-beta or later. Runtime 0.31 compiles single agents only: it
  refuses `flow.json`, `agents/`, `plugins/`, and `extends`. The file format is
  `agent-folder/1`.
</Callout>

## The loop [#the-loop]

| Step         | Command                                                                                     | Does                                                                                                                                                                                                 |
| ------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Write        | `nylorun init <path> [--description <text>] [--example]`                                    | Writes `<path>/AGENT.md` with its frontmatter and a line of instructions. The agent is named after the path's last segment.                                                                          |
| Check        | `nylorun check [<dir>...] [--fix] [--json]`                                                 | Compiles each folder with the Runtime's compiler and prints every problem by file, line, and column. Reads no Runtime. `--fix` makes the [fixable edits](#fix); `--json` is for [CI](#in-ci).        |
| Save and run | `nylorun apply [<dir>...] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]]` | Checks each folder, plans the save on the Runtime, then saves it under `If-Match`. `--dry-run` prints the plan and the compiled manifest and saves nothing. `--agent` then makes or moves the Agent. |
| Talk         | `nylorun chat <agent> [--session <id>] [--message <text>] [--owner <user>]`                 | Sends a message to the Agent and prints the reply as it comes.                                                                                                                                       |

`init <path>` names the agent after the path's last segment: `init agents/hello`
writes `agents/hello/AGENT.md` for the agent `hello`. `--example` also writes
`tools/get_order.json` (an HTTP tool), `mcp.json` (a server with a
`Bearer ${ORDERS_MCP_TOKEN}` header), `triggers/new-order.md` (a webhook
trigger), and a `README.md` that explains each. The first run in a project,
the nearest directory with `.git`, also writes `.gitattributes`
(`* text=auto eol=lf`), adds `.nylorun/credentials.json` to `.gitignore`, and
maps the folder's JSON files to their [schemas](#editor-schemas) in
`.vscode/settings.json`, each only when it is missing:

```text
$ npx nylorun init agents/hello --example
Wrote agents/hello/: AGENT.md, tools/get_order.json, mcp.json, triggers/new-order.md, README.md.
Wrote .gitattributes (* text=auto eol=lf), so a Windows checkout keeps LF line endings.
Added .nylorun/credentials.json to .gitignore.
Mapped the agent folder files to their JSON Schemas in .vscode/settings.json.
Next: nylorun check agents/hello, then nylorun apply agents/hello --agent to save it and make the Agent hello, then nylorun chat hello.
```

`check` and `apply` take a folder that holds `AGENT.md`, or a folder of such
folders: `npx nylorun check agents/` checks each subfolder with an `AGENT.md`.
`check` exits 1 when a folder does not compile. `apply` checks every folder
before it sends anything, so one broken folder stops the whole apply. The
example compiles, with a slot derived for each credential:

```text
$ npx nylorun check agents/hello
agents/hello/mcp.json:7:26 info: A bearer slot ORDERS_MCP_TOKEN is derived for https://orders.example.com/mcp
agents/hello/tools/get_order.json:20:19 info: A bearer slot ORDERS_TOKEN is derived for https://orders.example.com/lookup
agents/hello/README.md note: Kept with the source; not part of the agent
agents/hello: ok, agent manifest hello (5 files, agent-folder/1)
```

`orders.example.com` does not answer: replace the example tool and server
with your own, or delete them, before you talk to the Agent.

`apply --agent [<id>]` makes the Agent `<id>` from the manifest after the
save, by default named after the manifest, or moves an existing one to the
version saved, as `agents put` does. It does so also when the save is
`unchanged`, and prints what the Agent needs a person for, such as a required
secret with no value. `<id>` needs one folder; with several folders and no id,
each Agent is named after its manifest. Write `--agent=<id>` when `<id>` is
also a directory name.

`apply --watch` checks and plans again on each change to the folders, and to
the files their `runtime.json` [extends](#extends), until interrupted. With
`--save` it also saves each change. Each save is a new version that moves
every Agent tracking the manifest, so use it on a development manifest. A plan
prints a change to the instructions as a text diff, in `-` and `+` lines.

`chat <agent>` opens a new session on the Agent, or the one `--session <id>`
names, which is created with that id when it does not exist, so one session
can carry across runs. It sends `--message` once, or each line of standard
input, and prints the reply as it comes. The session's owner is `--owner`,
else the Agent's owner, else `--as`, else `$USER`. A turn that waits for a
person prints how to answer it with `nylorun sessions`, and a turn that fails
exits 1. To send JSON input, such as a flow's, use
`nylorun sessions send <id> --data <json>`.

After the first save, the Agent tracks the manifest, so each later `apply`
moves it to the new version. Write each secret slot's value, then talk to it:

```sh
npx nylorun apply agents/triage --agent teams/triage
printf %s "$GITHUB_TOKEN" | npx nylorun agents secrets put teams/triage GITHUB_TOKEN
npx nylorun chat teams/triage --message "Triage acme/api#42"
```

`agents put teams/triage --manifest triage`, `sessions create`, and
`sessions send` do the same step by step. Use `agents put` to make a second
Agent from the manifest, or to pin one to a version. See
[Agents](/docs/run/agents).

The commands act on the project's local Tenant, or on the Runtime that
`NYLORUN_RUNTIME_URL` and `NYLORUN_SERVER_KEY` name. See
[CLI](/docs/run/cli#which-installation).

## Layout [#layout]

```text
support/
├── AGENT.md              required: frontmatter, and the instructions as its body
├── instructions/*.md     more instructions, after the body
├── runtime.json          the runtime section
├── mcp.json              remote MCP servers, in Agent Plugins syntax
├── tools/<name>.json     one HTTP tool per file
├── triggers/<id>.md      one trigger per file; the body is its prompt
├── skills/<name>/        one Agent Skill per folder, with its SKILL.md
├── files/**              files copied under /workspace on a pod's new disk
├── schemas/*.json        JSON Schemas that files name by path
├── *.schema.json         JSON Schemas, such as output.schema.json
├── agents/<id>/          an agent it uses as a tool, or one of a flow's agents
├── flow.json             makes the folder a flow
├── plugins/<name>/       a vendored Agent Plugin
├── .nyloignore           what packing leaves out
└── anything else         kept with the source, not compiled (README.md, docs/)
```

Only `AGENT.md` is required. The directory gives a file its kind, and the file
name its name: `tools/lookup_order.json` is the HTTP tool `lookup_order`,
`triggers/nightly.md` the trigger `nightly`, and `agents/billing/` the agent
`billing`. Prose is Markdown with YAML frontmatter; everything structured is
JSON. [Subagents](#subagents), [flows](#flows), and [plugins](#plugins) are
folders of the same format inside the folder.

## AGENT.md [#agentmd]

```markdown title="AGENT.md"
---
name: triage
description: Reads a GitHub issue and proposes a label and a priority.
outputSchema: ./output.schema.json
mcpServers:
  github:
    tools: { "*": { enabled: false }, issue_read: { enabled: true } }
memory:
  access: read_only
---
Triage the GitHub issue the user names as owner/repo#number. Read it, and its
comments, with the github tools.
```

| Key            | Type                                                                                                                                                                                                          | Default   | Manifest field                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------ |
| `name`         | Required. 1 to 64 letters, digits, `.`, `_`, and `-`, starting with a letter or digit.                                                                                                                        |           | `name`                                           |
| `description`  | Required. A non-empty string. At most the Tenant's `manifestDescriptionChars` (1024 by default), checked at save.                                                                                             |           | `description`                                    |
| `format`       | `1`.                                                                                                                                                                                                          | `1`       | None: the compiler's format.                     |
| `harness`      | A harness id: a lowercase letter, then 1 to 62 lowercase letters, digits, and `-`. Any harness other than `nylorun` needs a `runtime.json` of kind `pod` with an `image`. See [Harness](/docs/build/harness). | `nylorun` | `harness.id`                                     |
| `inputSchema`  | A flow's input: a path to a [JSON Schema file](#schema-files), or the schema inline. A flow only; elsewhere it is `input_schema_refused`. See [Flows](#flows).                                                | None      | `harness.inputSchema`                            |
| `outputSchema` | A path to a [JSON Schema file](#schema-files), or the schema inline.                                                                                                                                          | None      | `harness.outputSchema`                           |
| `mcpServers`   | This agent's settings for each server of `mcp.json`. See [MCP servers](#mcpjson).                                                                                                                             | None      | `harness.mcpServers.<server>.deferred`, `.tools` |
| `skills`       | This agent's settings for each skill of `skills/`. See [Skills](#skills).                                                                                                                                     | None      | `harness.skills.<name>.secrets`                  |
| `memory`       | `{ access, instructions }`: `access` is `read_write` or `read_only`, `instructions` at most 4096 characters. See [Agent memory](/docs/run/memory).                                                            | Off       | `harness.memory`                                 |
| `loop`         | A map: the harness's own settings, passed to it with each turn. A key the `nylorun` harness does not read is a `loop_key` warning.                                                                            | None      | `harness.loop`                                   |
| `metadata`     | A map of your own.                                                                                                                                                                                            | None      | `metadata`                                       |

The manifest is saved under `name`. Over HTTP, `name` must equal the name in
the path. The folder's own name does not matter.

The body is the first instruction. `instructions/*.md` follow, one instruction
per file, in natural order: `2-style.md` before `10-tone.md`. Leading and
trailing blank lines are trimmed and nothing else is changed. An empty file
adds no instruction, and an agent with no instruction at all is an
`instructions_empty` warning. `instructions/` holds Markdown files only, with
no subfolders. See [Instructions](/docs/build/instructions). A flow's
`AGENT.md` has frontmatter only: see [Flows](#flows).

### Frontmatter [#frontmatter]

Frontmatter sits between two `---` lines at the top of the file. It is YAML
1.2 with the core schema: no anchors, aliases, tags, or merge keys, and a key
written twice is an error. Quote a value that starts with `*` or `&`, such as
a cron (`schedule: "0 2 * * *"`). An unknown key is an error that names the
closest known key, and `check --fix` renames it when exactly one is close.

Write `AGENT.md` and every other file as UTF-8. A byte order mark is ignored.
A file with CRLF line ends is read as LF, with a `crlf` note that
`check --fix` resolves; the `.gitattributes` that `nylorun init` writes
(`* text=auto eol=lf`) keeps a Windows checkout LF.

## runtime.json [#runtimejson]

```json title="runtime.json"
{
  "kind": "virtual",
  "network": { "allowedHosts": ["api.github.com"] }
}
```

`runtime.json` is the manifest's `runtime` section, with the same fields:
`kind`, `image`, `resources`, `storage`, `network`, `lifecycle`, `setup`,
`files`, `limits`, `secrets`, `triggers`, and `selfWake`. See
[Saving manifests](/docs/run/agent-manifests#fields). It also takes `$schema`
(see [Editor schemas](#editor-schemas)) and [`extends`](#extends), and the
compiler leaves both out of the manifest.

Without `runtime.json`, an Agent made from the manifest is kind `none`. A
folder without one that still needs a runtime section, for a
[derived slot](#secret-slots), a trigger, or `files/`, compiles with
`{ "kind": "none" }` plus those.

### extends [#extends]

```json title="agents/analyst/runtime.json"
{
  "$schema": "https://docs.nylorun.com/schemas/agent-folder/v1/runtime.json",
  "extends": "../../platform/pod.runtime.json",
  "resources": { "memory": "4GiB" }
}
```

```json title="platform/pod.runtime.json"
{
  "kind": "pod",
  "image": "python:3.13-slim",
  "resources": { "cpus": 2, "memory": "2GiB" },
  "network": { "allowedHosts": ["pypi.org", "files.pythonhosted.org"] }
}
```

`extends` names a shared runtime file outside the folder, by a path from the
folder. `nylorun check`, `nylorun apply`, and `packFolder` merge it under the
folder's file: the folder's keys win, objects merge, and arrays replace. The
folder above runs on a pod with 2 CPUs and `4GiB`. They send the merged
`runtime.json`, and record the files it came from in `.nylorun/origin.json`,
which is uploaded with the folder, so each saved version holds everything it
was compiled from. A missing file or a cycle is a `FolderError`.

The Runtime refuses an unresolved `extends` (`not_supported`: "resolve extends
before uploading"), so a client that uploads a folder itself merges it first.

## mcp.json [#mcpjson]

```json title="mcp.json"
{
  "mcpServers": {
    "github": {
      "type": "streamable-http",
      "url": "https://api.githubcopilot.com/mcp/",
      "headers": {
        "Authorization": "Bearer ${GITHUB_TOKEN}",
        "X-MCP-Toolsets": "issues",
        "X-MCP-Readonly": "true"
      }
    }
  }
}
```

`mcp.json` holds remote MCP servers in
[Agent Plugins](https://agent-plugins.org/plugin-authors/mcp-servers) syntax,
keyed by name. It takes `mcpServers` and an optional `$schema`: ours or
Agent Plugins' `https://agent-plugins.org/schemas/1.0.0/mcp.schema.json` (see
[Editor schemas](#editor-schemas)).

| Server key | Value                                 |
| ---------- | ------------------------------------- |
| `type`     | Required. `streamable-http` or `sse`. |
| `url`      | Required. The server's URL.           |
| `headers`  | A map of header names to strings.     |

A server's name is 1 to 64 letters, digits, `_`, and `-`. The Runtime runs
remote servers only, so a server with no `type`, or of type `stdio` or `ws`,
is refused. Claude Code's `"type": "http"` is refused with the fix: write
`"streamable-http"`. See [MCP](/docs/build/mcp).

### Credentials [#credentials]

`${NAME}` in a header names the secret slot the server authenticates with. It
is never read from your environment. The header leaves the manifest, and the
server gets `credential: "NAME"`:

| Header                                      | In the manifest                       | Slot                                                              |
| ------------------------------------------- | ------------------------------------- | ----------------------------------------------------------------- |
| `"Authorization": "Bearer ${GITHUB_TOKEN}"` | removed; `credential: "GITHUB_TOKEN"` | `bearer`, for the server's URL                                    |
| `"X-Api-Key": "${ACME_KEY}"`                | removed; `credential: "ACME_KEY"`     | `headers`, for the server's URL, described as setting `X-Api-Key` |
| `"X-MCP-Readonly": "true"`                  | kept as written                       | none                                                              |

Refused:

* `Bearer ${NAME}` in a header other than `Authorization`.
* `${NAME}` inside a longer value, other than `Bearer ${NAME}`.
* A default, `${NAME:-dev}`: a secret has none.
* `${…}` in `url`.
* Two credentials on one server.
* A name outside 1 to 64 letters, digits, `.`, `_`, and `-`.

A header without `${…}` is saved as written, in the manifest and in the
source, where anyone with `agents:read` can read it. Name a token as `${NAME}`
and write its value to the Agent's secrets instead. A literal value in an
`Authorization`, `X-Api-Key`, `Api-Key`, `Cookie`, or `*-Token` header, or a
token-shaped string (`ghp_`, `github_pat_`, `sk-`, `xox…-`, a JWT) in any
header, is a `secret_literal` warning. It warns and never refuses:

```text
agents/acme/mcp.json:7:22 warning: X-Api-Key holds a literal value, not a ${NAME} slot. The folder is kept byte-exact as the version's source, readable by anyone with agents:read, and the value goes into the manifest as written. Fix: write "${NAME}" and set the value in the Agent's vault
```

### This agent's settings [#this-agents-settings]

How this agent uses a server is in `AGENT.md`, under `mcpServers`, keyed by the
server's name in `mcp.json`:

| Key          | Value                                                                                                                                                              |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `deferred`   | `true` or `false` defers every tool of the server, or none. See [Deferred tools](/docs/build/mcp#deferred-tools).                                                  |
| `tools`      | Settings per tool, keyed by the server's own tool names, `"*"` for the rest: `enabled` and `deferred`. See [Per-tool settings](/docs/build/mcp#per-tool-settings). |
| `credential` | A plugin's server only: the secret slot it authenticates with. A server in `mcp.json` names its credential in a header instead. See [plugins/](#plugins).          |

A name that `mcp.json` does not declare is an error. Which calls wait for a
person is the Tenant's [tool policy](/docs/run/tool-policy), which names an MCP
tool by its server and its own name, `{ "server": "github", "tool": "issue_read" }`.
`approval` in a tool's settings is an `approval_removed` error (see
[tools/](#tools)).

## tools/ [#tools]

```json title="tools/get_order.json"
{
  "description": "Read one order by its id.",
  "inputSchema": {
    "type": "object",
    "properties": { "orderId": { "type": "string" } },
    "required": ["orderId"]
  },
  "outputSchema": "./schemas/order.json",
  "http": {
    "url": "https://orders.example.com/orders/{orderId}",
    "method": "GET",
    "credential": "ORDERS_TOKEN"
  }
}
```

Each `tools/<name>.json` is one [HTTP tool](/docs/build/tools) named after its
file: 1 to 64 letters, digits, `_`, and `-`, with no `__`.

| Key            | Type                                                                               | Default |
| -------------- | ---------------------------------------------------------------------------------- | ------- |
| `description`  | What the tool does, for the model.                                                 | None    |
| `inputSchema`  | Required. A JSON Schema, inline or a path to a [schema file](#schema-files).       |         |
| `outputSchema` | A JSON Schema, inline or a path. A JSON answer that does not match fails the call. | None    |
| `http`         | Required. The request.                                                             |         |

| `http` key   | Value                                                                                                                         | Default |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------- |
| `url`        | Required. The URL, whose path may name input fields as `{field}`. See [The request](/docs/build/tools#the-request).           |         |
| `method`     | `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`.                                                                                   | `POST`  |
| `credential` | A secret slot bound to `url`. See [Secret slots](#secret-slots).                                                              | None    |
| `timeoutMs`  | Milliseconds, at most the Tenant's [Runtime limit](/docs/run/limits) `httpToolTimeoutMs` (300 s by default), checked at save. | 60 s    |

A tool file takes no `name`, since its file name is its name. It may take
`$schema` (see [Editor schemas](#editor-schemas)). `tools/` holds `.json`
files only, with no subfolders.

`approval` is not a key of an agent manifest. In a tool file, or in an MCP
tool's settings in `AGENT.md`, it is an `approval_removed` error at its line:

```text
agents/orders/tools/get_order.json:4:3 error: approval is not set in an agent folder: tool approvals are the Tenant's tool policy, decided at the Tool Gate for every agent. To have a person approve a call, add a rule with "effect": "ask" for the tool to the Tenant's tool policy (nylorun tenant tool-policy put <file>, or PUT /v1/tenant/tool-policy)
```

Hold or refuse calls with the Tenant's [tool policy](/docs/run/tool-policy),
which names an HTTP tool by its file name: `{ "effect": "ask", "tool": "get_order" }`.
`agent` in a tool file is `tool_invalid`: an agent used as a tool is a folder,
[`agents/<id>/`](#subagents).

## triggers/ [#triggers]

```markdown title="triggers/nightly.md"
---
schedule: "0 2 * * *"
timezone: Asia/Kolkata
session: new
---
Triage every issue opened in acme/api since yesterday, one line each.
```

Each `triggers/<id>.md` is one [trigger](/docs/run/triggers) named after its
file: 1 to 64 letters, digits, `.`, `_`, and `-`, starting with a letter or
digit. The body is the prompt each run sends, and must not be empty.

| Key        | Value                                                                               |
| ---------- | ----------------------------------------------------------------------------------- |
| `schedule` | A five-field cron, quoted.                                                          |
| `timezone` | An IANA time zone, with `schedule` only. Default UTC.                               |
| `at`       | One run, at an RFC 3339 time with an offset, such as `"2026-11-01T09:00:00+05:30"`. |
| `webhook`  | `standard-webhooks`. See [Webhooks](/docs/run/triggers#webhooks).                   |
| `session`  | Required. A session id of the Agent, or `new` for a new session per run.            |

A trigger has exactly one of `schedule`, `at`, and `webhook`. A trigger file
needs frontmatter. One id in both `triggers/` and `runtime.json`'s `triggers`
is an error.

`check` reads a cron and a time zone as the Runtime does, in `triggers/*.md`
and in `runtime.json`'s `triggers`: a cron that does not parse is
`schedule_invalid`, and an unknown time zone `timezone_invalid`, with a
did-you-mean. An `at` in the past, and a schedule that fires more often than
the Tenant allows, are refused only when saved.

## skills/ [#skills]

```text
skills/open-pr/
├── SKILL.md
└── scripts/open.sh
```

Each `skills/<name>/` is an [Agent Skill](/docs/build/skills), hashed and
mounted as any skill is. Its `SKILL.md` frontmatter takes the Agent Skills keys
only:

| Key                                                     | Value                                                                                        |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `name`                                                  | Required. 1 to 64 lowercase letters, digits, and single hyphens, equal to the folder's name. |
| `description`                                           | Required. At most 1024 characters.                                                           |
| `license`, `compatibility`, `metadata`, `allowed-tools` | Optional. Not part of the manifest.                                                          |

Which secret slots a skill's scripts read is this agent's choice, so it is in
`AGENT.md`, not `SKILL.md`:

```markdown title="AGENT.md"
---
name: releaser
description: Opens release pull requests.
skills:
  open-pr: { secrets: [GH_TOKEN] }
---
```

A `SKILL.md` with `secrets` is a `skill_secrets` error, fixable with
`check --fix`, which moves the list to `AGENT.md`'s `skills` block when
`AGENT.md` lists none for the skill, or lists them all:

```text
$ npx nylorun check agents/releaser
agents/releaser/skills/open-pr/SKILL.md:4:1 error: secrets is not an Agent Skills key: which secrets a skill gets is the agent's choice. Fix: move it to AGENT.md's skills block, as skills: { open-pr: { secrets: [...] } } (fixable with --fix)
agents/releaser: 1 error
```

When `AGENT.md` writes `skills` in flow style, such as `skills: {}`, `--fix`
leaves it and says to add the list by hand.

Each secret is an environment variable name, and must be an
`environment_secret` or `environment_variable` slot of `runtime.json`. Skills
need a sandbox: `runtime.json` of kind `virtual` or `pod`. `check` checks both
(`manifest_inconsistent`, at the field). See
[Shell credentials](/docs/run/credentials#shell-credentials).

## files/ [#files]

Each file under `files/` is copied to the same path under `/workspace` when a
pod's disk is made or reset: `files/data/orders.csv` lands at
`/workspace/data/orders.csv`. They become one `runtime.files` entry,
`{ "path": "/workspace", "files": { … } }`, by hash, after any `files` that
`runtime.json` lists. `files/` needs `runtime.json` of kind `pod`, and
`network` a sandbox kind; `check` refuses either otherwise (`runtime_kind`).
See [Setup and input files](/docs/run/agent-manifests#setup-and-input-files).

## Schema files [#schema-files]

`inputSchema` and `outputSchema` in `AGENT.md` and in a tool file take a JSON
Schema inline or a path to a schema file. A schema file is any `.json` file
under `schemas/`, or a `*.schema.json` file outside the reserved directories.
It holds one JSON object.

A path names a file from the folder's root, whichever file names it:
`./output.schema.json`, `output.schema.json`, and `/output.schema.json` are
the same file, and a tool file names `./schemas/order.json`. A path with `..`
is refused. The schema is copied into the manifest; the file itself is kept
with the source.

Every schema, inline or a file, is checked as JSON Schema 2020-12 at its line
and column (`json_schema_invalid`):

```text
agents/bad/AGENT.md:6:25 error: type "objekt" is not a JSON Schema type. Did you mean 'object'? The types are null, boolean, object, array, number, string and integer
```

It is also checked as the engine reads it each turn: `if` and `then`, `not`,
`dependentRequired`, `unevaluatedProperties`, and a `$ref` outside `$defs` are
refused. A keyword close to a known one, such as `proprties`, is a
`json_schema_keyword` warning that `check --fix` renames. A schema file's own
`$schema` is its dialect and stays.

## Editor schemas [#editor-schemas]

Every JSON file of a folder may start with `$schema`, for an editor; the
compiler leaves it out of the manifest. Each kind of file has a JSON Schema
2020-12, built from what the compiler checks:

| File                           | Schema                                                                                                                                                        |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AGENT.md` frontmatter         | [`/schemas/agent-folder/v1/agent-md.json`](/schemas/agent-folder/v1/agent-md.json)                                                                            |
| `triggers/<id>.md` frontmatter | [`/schemas/agent-folder/v1/trigger-md.json`](/schemas/agent-folder/v1/trigger-md.json)                                                                        |
| `runtime.json`                 | [`/schemas/agent-folder/v1/runtime.json`](/schemas/agent-folder/v1/runtime.json), or [`/schemas/runtime-manifest/v1.json`](/schemas/runtime-manifest/v1.json) |
| `tools/<name>.json`            | [`/schemas/agent-folder/v1/tool.json`](/schemas/agent-folder/v1/tool.json)                                                                                    |
| `mcp.json`                     | [`/schemas/agent-folder/v1/mcp.json`](/schemas/agent-folder/v1/mcp.json), or Agent Plugins' `mcp.schema.json`                                                 |
| `flow.json`                    | [`/schemas/agent-folder/v1/flow.json`](/schemas/agent-folder/v1/flow.json)                                                                                    |

Each URL is on `https://docs.nylorun.com`, such as
`https://docs.nylorun.com/schemas/agent-folder/v1/runtime.json`. A Markdown
file takes no `$schema`: the two frontmatter schemas are for an editor that
checks YAML frontmatter. A `$schema` that names another kind of file, or a
schema that is not one of these, is a `schema_url` warning, and the file is
read by its path either way. The schemas are also in `@nylorun/core`, as
`@nylorun/core/schemas/agent-folder-<kind>.json`.

`nylorun init agents/hello` maps `agents/**/runtime.json`,
`agents/**/tools/*.json`, `agents/**/flow.json`, and `agents/**/mcp.json`
(outside `plugins/`) to their schemas in `.vscode/settings.json`.

## Secret slots [#secret-slots]

An HTTP tool's `http.credential`, a `${NAME}` in an `mcp.json` header, and a
plugin server's `credential` in `AGENT.md` name a secret slot bound to that
URL. When `runtime.json` does not declare the slot,
the compiler derives one and reports it as `info`:

```json
{ "type": "bearer", "url": "https://api.githubcopilot.com/mcp/", "required": true }
```

A `${NAME}` alone in a header derives a `headers` slot instead. A slot that
`runtime.json` declares wins, and `check` checks it as the save does: a
`bearer` or `headers` slot bound to the exact URL. A slot binds one URL, path
and query included, so one credential name on two URLs, such as
`tools/get_order.json` and `tools/refund.json`, is an error naming both files:
give each URL its own slot.

Write each slot's value into the Agent:
`npx nylorun agents secrets put <agent> <name>`, or
`client.agents.secrets.put`. Until a required slot has a value, the Agent's
state is `input_required`, with `secret_unset` in its `attention`. See
[Secret slots](/docs/run/agent-manifests#secret-slots).

## Subagents [#subagents]

Without `flow.json`, each `agents/<id>/` beside `AGENT.md` is an agent manifest
offered to the agent as the tool `<id>`, with the subagent's description and
the input `{ task }`, as [`.tools(child)`](/docs/build/subagents) offers an
agent used as a tool. It runs on the session's Agent, in its runtime. See
[An agent used as a tool](#an-agent-used-as-a-tool).

`<id>` takes letters, digits, `_`, and `-`, up to 64 characters
(`agent_name`), and must not also name a `tools/<id>.json` (`tool_twice`). A
subagent is an agent manifest without what belongs to the folder it is in:

| In a subagent                                               | Refused as                                                                    |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `runtime.json`, `triggers/`, `files/`                       | `nested_refused`: they are the outer folder's.                                |
| `format`, `harness`, `memory`, or `loop` in its `AGENT.md`  | `nested_refused`: it runs in the outer agent's harness.                       |
| `agents/` of its own                                        | `subagent_depth`: agents used as tools nest one level deep.                   |
| A credential on its MCP servers, or `secrets` on its skills | `credential_unsupported`: only the outer agent's servers and skills get them. |

A subagent with its own `flow.json` is a flow offered as a tool, and its
stages name the agents under its own `agents/`.

## Flows [#flows]

`flow.json` makes the folder a [flow agent](/docs/build/flows)
(`harness.kind: "workflow"`). It holds one node, the flow's root, and each
node has exactly one of these keys, and an optional `id`:

| Node                                                | Runs                                                                                                              | Builder                                        |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `{ "agent": "<id>" }`                               | The agent `agents/<id>/` of the flow root.                                                                        | An agent stage                                 |
| `{ "tool": "<name>" }`                              | The HTTP tool `tools/<name>.json` beside this `flow.json`.                                                        | An [HTTP stage](/docs/build/flows#http-stages) |
| `{ "chain": [ … ] }`                                | Each node in order, each one's output the next one's input.                                                       | [Pipe](/docs/build/pipe)                       |
| `{ "switch": { "cases": { … }, "default": … } }`    | The one case the previous stage's output names.                                                                   | [Switch](/docs/build/switch)                   |
| `{ "parallel": { "<key>": … } }`                    | Each named branch at once, on the same input.                                                                     | [Parallel](/docs/build/parallel)               |
| `{ "map": { "each": … } }`                          | The node once per item of the previous stage's output.                                                            | [Map](/docs/build/map)                         |
| `{ "loop": { "run": …, "verify": …, "max": <n> } }` | `run`, then `verify`, until it passes or `max` runs. `verify` is `{ "agent": "<id>" }` or `{ "tool": "<name>" }`. | [Loop](/docs/build/loop)                       |

Every `flow.json` under the flow root names its stages from the root's one
`agents/`, by id. An `agents/<id>/` with its own `flow.json` is a sub-flow over
the same agents, so it has no `agents/` of its own (`flow_namespace`), and a
sub-flow that runs itself is `flow_cycle`. An id with no folder is
`agent_unknown`, and a tool with no file `tool_unknown`, each with a
did-you-mean. A tool file no node names is `tool_unused`: a flow holds only
the tools its nodes name.

The flow's `AGENT.md` has frontmatter only (`flow_body`), and may set
`inputSchema`, the flow's input. A flow has no `instructions/`, `mcp.json`,
`skills/`, or `plugins/`, and its `AGENT.md` no `mcpServers`, `skills`, or
`memory` (`flow_refused`): each of its agents has its own. The flow root keeps
`runtime.json`, `triggers/`, and `files/`, and its agents run in that runtime.
An agent of `agents/` that no `flow.json` names is an `agent_unused` warning,
and is left out of the manifest. See [A flow](#a-flow).

## Plugins [#plugins]

`plugins/<name>/` holds an [Agent Plugin](/docs/build/plugins) as its author
published it. The compiler folds it in as `.plugin()` does: its skills and its
`mcp.json` servers join the agent's, each labelled with the plugin's name and
version, and `harness.plugins` lists the plugin.

```text
pr-bot/
├── AGENT.md
├── runtime.json
└── plugins/github/
    ├── plugin.json          the Agent Plugins $schema, name, version
    ├── mcp.json             the Agent Plugins MCP $schema, and its servers
    ├── skills/open-pr/SKILL.md
    └── hooks/hooks.json     not run: a plugin_part_ignored note
```

```json title="pr-bot/plugins/github/plugin.json"
{
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
  "name": "github",
  "version": "1.2.0",
  "description": "GitHub's MCP server and a skill that opens pull requests."
}
```

```text
$ npx nylorun check pr-bot
pr-bot/plugins/github/mcp.json:7:37 info: A bearer slot GITHUB_TOKEN is derived for https://api.githubcopilot.com/mcp/
pr-bot/plugins/github/hooks/ note: Hooks are not run by Nylorun
pr-bot: ok, agent manifest pr-bot (6 files, agent-folder/1)
```

A plugin needs `plugin.json` with the Agent Plugins `$schema` and a `name`,
and its `mcp.json` the Agent Plugins MCP `$schema` (`plugin_invalid`). An
invalid skill or server is an error, as in the agent's own `skills/` and
`mcp.json`. A skill or server whose name the agent already has is
`plugin_duplicate`. What Nylorun does not run, such as hooks, commands, and
Claude Code's `agents/*.md`, is kept with the source with a
`plugin_part_ignored` note, and a `plugin.json` field Agent Plugins 1.0.0 does
not have is a `plugin_unknown_key` warning. A flow takes no plugins: vendor a
plugin in the agent that uses it.

## .nyloignore [#nyloignore]

```text title=".nyloignore"
notes/
*.log
```

`.nyloignore` in a folder names, in gitignore syntax, the files that packing
leaves out: `nylorun check`, `nylorun apply`, and `packFolder` neither compile
nor upload them. Packing always skips `.git`, `.hg`, `.svn`, and
`node_modules`, and follows no symbolic link. `.nyloignore` itself is kept with
the source.

## How a folder maps to the manifest [#how-a-folder-maps-to-the-manifest]

| Folder                                       | Agent manifest v8                                                           |
| -------------------------------------------- | --------------------------------------------------------------------------- |
| `AGENT.md` `name`, `description`, `metadata` | `name`, `description`, `metadata`                                           |
| `AGENT.md` `harness`                         | `harness.id`                                                                |
| `AGENT.md` body, then `instructions/*.md`    | `harness.instructions`, one string per file                                 |
| `AGENT.md` `outputSchema`, `memory`, `loop`  | `harness.outputSchema`, `harness.memory`, `harness.loop`                    |
| `AGENT.md` `inputSchema` (a flow)            | `harness.inputSchema`                                                       |
| `mcp.json`, with `AGENT.md` `mcpServers`     | `harness.mcpServers`, `credential` from `${NAME}`                           |
| `skills/<name>/`, with `AGENT.md` `skills`   | `harness.skills.<name>`, with `files` by hash and `secrets`                 |
| `tools/<name>.json`                          | `harness.tools[]`, an HTTP tool named `<name>`                              |
| `agents/<id>/`, with no `flow.json`          | `harness.tools[]`, an agent used as a tool named `<id>`                     |
| `flow.json`, with `agents/`                  | `harness.kind: "workflow"`, `harness.root`, and `harness.agents`            |
| `plugins/<name>/`                            | Its skills and MCP servers, labelled with the plugin, and `harness.plugins` |
| `runtime.json`, after `extends`              | `runtime`                                                                   |
| A derived slot                               | `runtime.secrets.<name>`                                                    |
| `triggers/<id>.md`                           | `runtime.triggers.<id>`, with the body as `prompt`                          |
| `files/**`                                   | `runtime.files[]`, `{ path: "/workspace", files }`                          |
| Anything else                                | Nothing; kept with the source                                               |

`nylorun apply <dir> --dry-run` prints the compiled manifest, and
`nylorun check <dir> --json` prints it offline. The same folder always
compiles to the same manifest, whatever order its files are listed in.

## Examples [#examples]

### An agent [#an-agent]

```text
writer/
├── AGENT.md
├── instructions/01-style.md
├── output.schema.json
└── README.md
```

```markdown title="writer/AGENT.md"
---
name: writer
description: Writes a one-paragraph release note from a list of changes.
outputSchema: ./output.schema.json
metadata:
  team: docs
---
Write one paragraph that a customer can read, from the changes the user lists.
Leave out internal names.
```

```markdown title="writer/instructions/01-style.md"
Use the present tense. No marketing adjectives.
```

```json title="writer/output.schema.json"
{
  "type": "object",
  "properties": { "title": { "type": "string" }, "note": { "type": "string" } },
  "required": ["title", "note"]
}
```

It compiles to a manifest with no runtime section, so its Agents are kind
`none`. `README.md` is kept with the source.

### An HTTP tool and its secret [#an-http-tool-and-its-secret]

```text
orders/
├── AGENT.md
├── schemas/order.json
└── tools/get_order.json
```

```markdown title="orders/AGENT.md"
---
name: orders
description: Answers questions about one order.
---
Look the order up with get_order before you answer. Never guess a status.
```

`tools/get_order.json` is the [tool above](#tools), and `schemas/order.json`
its output schema. Its `credential` derives a `bearer` slot `ORDERS_TOKEN`
bound to `https://orders.example.com/orders/{orderId}`, with a runtime section
of kind `none`:

```sh
npx nylorun apply orders --agent teams/orders
printf %s "$ORDERS_TOKEN" | npx nylorun agents secrets put teams/orders ORDERS_TOKEN
```

The Runtime sends the Agent's `ORDERS_TOKEN` as `Authorization: Bearer …` on
each call. The tool's name in a tool policy rule is `get_order`.

### An MCP server [#an-mcp-server]

```text
triage/
├── AGENT.md
├── mcp.json
└── runtime.json
```

```markdown title="triage/AGENT.md"
---
name: triage
description: Reads a GitHub issue and proposes a label and a priority.
mcpServers:
  github:
    deferred: false
    tools: { "*": { enabled: false }, issue_read: { enabled: true } }
---
Triage the GitHub issue the user names as owner/repo#number. Read it, and its
comments, with the github tools.

Answer with one label (bug, feature, question or docs), a priority from p0
(urgent) to p3, and one sentence on why. Change nothing on GitHub.
```

`mcp.json` is the [file above](#mcpjson). `runtime.json` declares the slot
itself, with a description, so nothing is derived:

```json title="triage/runtime.json"
{
  "kind": "virtual",
  "network": { "allowedHosts": [] },
  "secrets": {
    "GITHUB_TOKEN": {
      "type": "bearer",
      "url": "https://api.githubcopilot.com/mcp/",
      "required": true,
      "description": "A GitHub token that may read issues, for GitHub's MCP server"
    }
  }
}
```

A smaller version, with no `runtime.json`, so that its slot is derived, is in
the `nylorun/agents` repository as
[`examples/agents/triage`](https://github.com/nylorun/agents/tree/main/examples/agents/triage).

### Triggers [#triggers-1]

```text
digest/
├── AGENT.md
├── mcp.json
├── triggers/nightly.md
└── triggers/alerts.md
```

```markdown title="digest/triggers/nightly.md"
---
schedule: "30 8 * * 1-5"
timezone: Asia/Kolkata
session: digest
---
Summarize the issues opened in acme/api since the last digest, one line each.
```

```markdown title="digest/triggers/alerts.md"
---
webhook: standard-webhooks
session: new
---
An alert arrived. Read it, find the issue it concerns, and say what to do.
```

`nightly` runs on weekdays at 08:30 in Kolkata, always in the session
`digest`. `alerts` runs once per signed delivery, each in a new session. Each
Agent made from the manifest has its own copy of both. Make the webhook
trigger's signing secret, shown once with its URL, with
`npx nylorun agents triggers new-secret <agent> alerts`. See
[Triggers and wakes](/docs/run/triggers).

### A flow [#a-flow]

```text
brief/
├── AGENT.md
├── flow.json
└── agents/
    ├── researcher/AGENT.md
    └── writer/AGENT.md
```

```markdown title="brief/AGENT.md"
---
name: brief
description: Researches a topic, then writes a short brief on it.
---
```

```json title="brief/flow.json"
{ "chain": [{ "agent": "researcher" }, { "agent": "writer" }] }
```

```markdown title="brief/agents/researcher/AGENT.md"
---
name: researcher
description: Lists what matters about the topic.
---
List the five facts about the topic a newcomer most needs, each with one sentence of context.
```

```markdown title="brief/agents/writer/AGENT.md"
---
name: writer
description: Writes the brief.
---
Write a brief of at most 200 words from the facts you are given, for a reader new to the topic.
```

```text
$ npx nylorun check brief
brief: ok, agent manifest brief (4 files, agent-folder/1)
```

The flow sends its input to `researcher`, then `researcher`'s answer to
`writer`, whose answer is the flow's. A flow that uses every node is
[`examples/agents/repo-brief-folder`](https://github.com/nylorun/agents/tree/main/examples/agents/repo-brief-folder)
in the `nylorun/agents` repository: a switch, a map, a parallel stage, a loop,
and an HTTP tool stage, in a sub-flow:

```json title="repo-brief-folder/flow.json"
{
  "chain": [
    { "agent": "triage" },
    { "id": "route", "switch": {
        "cases": { "repo": { "agent": "research" } },
        "default": { "agent": "decline" } } }
  ]
}
```

```json title="repo-brief-folder/agents/research/flow.json"
{
  "chain": [
    { "id": "ask", "map": { "each": { "agent": "researcher" } } },
    { "id": "review", "parallel": { "overview": { "agent": "overview" }, "risks": { "agent": "risks" } } },
    { "id": "draft", "loop": { "run": { "agent": "writer" }, "verify": { "agent": "editor" }, "max": 2 } },
    { "tool": "publish_brief" }
  ]
}
```

```text
$ npx nylorun check repo-brief-folder
repo-brief-folder: ok, agent manifest repo-brief (23 files, agent-folder/1)
```

### An agent used as a tool [#an-agent-used-as-a-tool]

```text
support/
├── AGENT.md
└── agents/billing/AGENT.md
```

```markdown title="support/AGENT.md"
---
name: support
description: Answers customer questions, and hands billing questions to billing.
---
Answer the customer's question. For a question about an invoice or a refund,
call billing with the question as the task, and pass on its answer.
```

```markdown title="support/agents/billing/AGENT.md"
---
name: billing
description: Answers one question about invoices and refunds.
---
Answer the billing question in the task in two sentences or fewer.
```

The model calls `billing` with `{ "task": "…" }`, and gets `billing`'s answer
as the tool's result. The manifest embeds `billing` in the tool, so one save
holds both.

## Diagnostics [#diagnostics]

Each problem comes back as `file:line:column severity: message`, every
problem the compiler finds in one run:

```text
$ npx nylorun check agents/bad
agents/bad/AGENT.md:4:1 error: Unknown key 'skils'. Did you mean 'skills'? (fixable with --fix)
agents/bad/AGENT.md:6:25 error: type "objekt" is not a JSON Schema type. Did you mean 'object'? The types are null, boolean, object, array, number, string and integer
agents/bad/AGENT.md note: The file has CRLF line ends, read as LF. Fix: save it with LF line ends (fixable with --fix)
agents/bad: 2 errors
```

Some errors stop the checks that depend on them, such as a `SKILL.md` with
`secrets`, an invalid schema, or a trigger that does not parse, so check again
after you fix them. A problem one edit fixes ends `(fixable with --fix)`.

| Severity  | Means                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `error`   | The folder does not compile, and nothing is sent.                                                                                          |
| `warning` | The folder compiles. Read it before you save. See [Warnings](#warnings).                                                                   |
| `info`    | Something the compiler added, such as a derived slot (`slot_derived`).                                                                     |
| `note`    | A file kept with the source and not compiled (`file_not_compiled`), a part of a plugin Nylorun does not run, or CRLF line ends read as LF. |

Messages name the field, what it takes, and the closest value:

```text
agents/typo/runtime.json:1:11 error: kind is "virtual", "pod" or "none", not "virtal". Did you mean 'virtual'?
```

The Runtime answers a folder that does not compile with
`422 folder_invalid`, every diagnostic in `details.diagnostics` with its
`code`, `file`, `line`, `column`, the manifest `pointer` when there is one,
and its `fix` when it has one.

### Refused by the compiler [#refused-by-the-compiler]

| Code                                                                                                   | When                                                                                                                                                                                        |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_missing`, `frontmatter_missing`                                                                 | No `AGENT.md` at the root, or one without frontmatter.                                                                                                                                      |
| `name_missing`, `name_invalid`, `name_mismatch`, `description_missing`                                 | `name` or `description` missing or invalid, or `name` other than the name saved under.                                                                                                      |
| `unknown_key`                                                                                          | A key the file does not take, with "Did you mean" or the known keys.                                                                                                                        |
| `value_invalid`, `harness_invalid`                                                                     | A frontmatter value of the wrong type, such as `metadata` that is not a map, or a bad `harness` id.                                                                                         |
| `json_invalid`, `frontmatter_invalid`, `not_utf8`                                                      | A file that does not parse: JSON comments, trailing commas, a key twice, YAML aliases or tags.                                                                                              |
| `wrong_name`, `wrong_syntax`                                                                           | `agent.md` in lower case, or `runtime.yaml`, `mcp.yaml`, `runtime.jsonc`, and the like.                                                                                                     |
| `reserved_directory`                                                                                   | A file `tools/`, `triggers/`, `instructions/`, `skills/`, or `schemas/` does not take, such as `tools/notes.txt`.                                                                           |
| `path_invalid`, `path_case`, `path_duplicate`, `path_refused`                                          | A path with `..`, `\`, control characters, or not in NFC; two paths that differ only in case; `.git/` or `node_modules/`.                                                                   |
| `file_missing`                                                                                         | A file the folder lists whose bytes the compiler was not given.                                                                                                                             |
| `mcp_type`, `mcp_invalid`                                                                              | A server with no `type`, `stdio`, `ws`, or `http`, or with no `url`.                                                                                                                        |
| `credential_placement`, `credential_default`, `credential_name`, `credential_twice`, `credential_urls` | A `${NAME}` the compiler cannot bind to a slot. See [Credentials](#credentials).                                                                                                            |
| `server_unknown`, `skill_unknown`                                                                      | `AGENT.md` names a server or skill the folder does not have.                                                                                                                                |
| `skill_invalid`, `skill_secrets`                                                                       | A skill without `SKILL.md`, with a bad `name` or `description`, or with `secrets`.                                                                                                          |
| `tool_invalid`                                                                                         | A tool file with `name` or `agent`, without `http` or `inputSchema`, or with a bad file name.                                                                                               |
| `approval_removed`                                                                                     | `approval` in a tool file or an MCP tool's settings. See [tools/](#tools).                                                                                                                  |
| `schema_missing`, `schema_invalid`, `path_outside`                                                     | A schema path to no file, to a file that is not a schema file, or outside the folder; or a field that breaks the manifest's schema.                                                         |
| `json_schema_invalid`                                                                                  | A schema that is not valid JSON Schema 2020-12, or uses what the engine does not read. See [Schema files](#schema-files).                                                                   |
| `schedule_invalid`, `timezone_invalid`                                                                 | A cron that does not parse, or an unknown time zone.                                                                                                                                        |
| `trigger_twice`, `prompt_empty`                                                                        | A trigger in both `triggers/` and `runtime.json`, or a trigger with an empty body.                                                                                                          |
| `manifest_inconsistent`                                                                                | Sections that do not fit, at the field: skills with no sandbox, a custom harness off a pod, a credential bound to another URL.                                                              |
| `runtime_kind`                                                                                         | `files/` off a pod, or `network` on kind `none`.                                                                                                                                            |
| `flow_invalid`, `flow_body`, `flow_refused`, `flow_namespace`, `flow_cycle`                            | A `flow.json` node that is not valid, a flow's `AGENT.md` with a body, what a flow does not take, `agents/` in a sub-flow, or a sub-flow that runs itself. See [Flows](#flows).             |
| `agent_unknown`, `agent_name`, `tool_unknown`, `tool_unused`, `tool_twice`, `input_schema_refused`     | A stage that names no agent or tool, an `agents/<id>/` with a bad id, a flow's tool no node names, an id that is both an agent and a tool, or `inputSchema` on an agent that is not a flow. |
| `nested_refused`, `subagent_depth`, `credential_unsupported`                                           | What a subagent or a flow's agent does not take. See [Subagents](#subagents).                                                                                                               |
| `plugin_invalid`, `plugin_duplicate`                                                                   | A plugin without a valid `plugin.json` or `mcp.json` `$schema`, or a skill or server name the agent already has. See [Plugins](#plugins).                                                   |
| `format_unsupported`                                                                                   | A `format` other than `1`.                                                                                                                                                                  |
| `not_supported`                                                                                        | An unresolved `extends`: resolve it before uploading. See [extends](#extends).                                                                                                              |
| `limit_exceeded`                                                                                       | Over a [limit](#limits).                                                                                                                                                                    |

### Warnings [#warnings]

A warning never stops a save.

| Code                  | When                                                                                                                              |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `secret_file`         | `.env` or `.env.*` anywhere, or a `.pem` or `.key` file outside `skills/` and `files/`.                                           |
| `secret_literal`      | A literal value in an authentication header, or a token-shaped string in any header. See [Credentials](#credentials).             |
| `tool_policy_file`    | A root `tool-policy.json`, `policy.json`, or `*.policy.json`. Tool policy is the Tenant's; the file is only kept with the source. |
| `schema_url`          | A `$schema` that names another kind of file, or a schema that is not ours. See [Editor schemas](#editor-schemas).                 |
| `json_schema_keyword` | A JSON Schema keyword close to a known one, such as `proprties`.                                                                  |
| `loop_key`            | A `loop` key the `nylorun` harness does not read.                                                                                 |
| `instructions_empty`  | An agent with no instructions.                                                                                                    |
| `agent_unused`        | An agent of a flow's `agents/` that no `flow.json` names. It is left out of the manifest.                                         |
| `plugin_unknown_key`  | A `plugin.json` field Agent Plugins 1.0.0 does not have.                                                                          |

Each secret warning says that the folder is kept byte for byte as the
version's source, readable by anyone with `agents:read`, and where a secret
goes instead: a `${NAME}` slot, with its value in the Agent's secrets.

```text
$ npx nylorun check agents/warn
agents/warn/.env warning: An environment file usually holds secrets. The folder is kept byte-exact as the version's source, readable by anyone with agents:read. A secret usually goes in a ${NAME} slot (in runtime.json, or a header in mcp.json), with its value set in the Agent's vault: leave this file out of the upload
agents/warn/AGENT.md:5:3 warning: Unknown loop setting 'maxSteps'. loop holds the harness's own settings, and the nylorun harness reads none yet, so it is ignored
agents/warn/AGENT.md:7:1 warning: The agent has no instructions: AGENT.md's body is empty and instructions/ holds none, so the model gets the harness's own instructions only. Write them below the frontmatter
agents/warn/tool-policy.json warning: This looks like a tool policy, and it is only kept with the source: tool policy is the Tenant's, for every agent, set with nylorun tenant tool-policy put <file> (PUT /v1/tenant/tool-policy)
agents/warn: ok, agent manifest warn (3 files, agent-folder/1)
```

Leave a secret file out of the upload with [`.nyloignore`](#nyloignore).

### What only the save checks [#what-only-the-save-checks]

`check` runs every rule that needs no Runtime, so a folder that passes it
fails a save only on what depends on the Tenant:

* The Tenant's [Runtime limits](/docs/run/limits): the description's length,
  the secret slots, a tool's `timeoutMs` against `httpToolTimeoutMs`, the
  hosts `network.allowedHosts` may name, and a trigger's interval, count, and
  prompt length.
* An `at` trigger in the past.
* Files the Runtime does not hold (`definition_files_missing`).
* Folder limits the Tenant set lower than the defaults (see [Limits](#limits)).

Run `npx nylorun apply <dir> --dry-run` to see them before a save. The plan
names each by its manifest path. See
[Sections that fit](/docs/run/agent-manifests#sections-that-fit).

### Fix [#fix]

`check --fix` makes three kinds of edit, and only those:

* Renames an unknown key that has exactly one did-you-mean, where it is, and
  keeps every other byte. This covers `AGENT.md`, a trigger, a `SKILL.md`, a
  tool file, `runtime.json`, `mcp.json`, `flow.json`, and a JSON Schema's
  keywords.
* Makes CRLF line ends LF.
* Moves `secrets` from a `SKILL.md` to `AGENT.md`'s `skills` block.

It prints each change by file, then checks again:

```text
$ npx nylorun check --fix agents/bad
fixed agents/bad/AGENT.md: CRLF line ends made LF
fixed agents/bad/AGENT.md: renamed 'skils' to 'skills' at line 4
agents/bad/AGENT.md:6:25 error: type "objekt" is not a JSON Schema type. Did you mean 'object'? The types are null, boolean, object, array, number, string and integer
agents/bad: 1 error
```

```text
$ npx nylorun check --fix agents/releaser
fixed agents/releaser/skills/open-pr/SKILL.md: secrets moved to AGENT.md's skills block
fixed agents/releaser/AGENT.md: skills.open-pr.secrets: GH_TOKEN, from skills/open-pr/SKILL.md
agents/releaser: ok, agent manifest releaser (3 files, agent-folder/1)
```

Without `--fix`, `check` writes nothing. `--fix` leaves a file whose bytes
changed since it was checked, and a `runtime.json` that `extends` another. In
code, `applyFolderFixes` in `@nylorun/core/folder` makes the same edits on
text.

### In CI [#in-ci]

`check --json` prints one JSON document, with the same exit codes:

```text
$ npx nylorun check agents/releaser --json
{
  "folders": [
    {
      "path": "agents/releaser",
      "ok": false,
      "diagnostics": [
        {
          "severity": "error",
          "code": "skill_secrets",
          "message": "secrets is not an Agent Skills key: which secrets a skill gets is the agent's choice. Fix: move it to AGENT.md's skills block, as skills: { open-pr: { secrets: [...] } }",
          "file": "skills/open-pr/SKILL.md",
          "line": 4,
          "column": 1,
          "fix": {
            "kind": "move_skill_secrets",
            "file": "skills/open-pr/SKILL.md",
            "skill": "open-pr",
            "secrets": [
              "GH_TOKEN"
            ]
          }
        }
      ]
    }
  ]
}
```

Each folder has its `path`, `ok`, the `manifest` it compiles to when it does,
and its `diagnostics`, each fixable one with its `fix`: `rename_key`, `crlf`,
or `move_skill_secrets`. With `--fix`, each folder also lists what was
`fixed`, and what was `unfixed` with why.

## Limits [#limits]

| Limit                                        | Default              | Runtime limit         |
| -------------------------------------------- | -------------------- | --------------------- |
| Files in a folder                            | 2,000                | `folderFiles`         |
| Bytes in a folder                            | 50 MiB               | `folderBytes`         |
| One Markdown or JSON file the compiler reads | 1 MiB                | `folderTextFileBytes` |
| A path                                       | 1 to 1024 characters | Fixed                 |

The first three are the Tenant's [Runtime limits](/docs/run/limits#agent-manifests),
set with `admin.settings.limits.put`, `nylorun tenant settings limits put`, or
Studio's **Limits** tab. A folder over `folderFiles` or `folderBytes` is
`413 limit_exceeded`, with `details.limit` naming the limit, and a file over
`folderTextFileBytes` is `422 folder_invalid`. `nylorun check`,
`nylorun apply`, and `packFolder` hold a folder to the defaults whatever the
Tenant sets, so a raised limit helps only a client that packs the folder
itself.

Each file is also uploaded as a definition file, within the Tenant's limits.
A skill's files count toward the skill limit, as any skill's do.

## The source [#the-source]

Each version saved from a folder keeps the folder as its source: every file,
byte for byte, `README.md` included. A version's `source` gives its
`compiler`, `files`, and `bytes`; a version saved as JSON has none. Reading it
takes `agents:read`.

A save whose compiled manifest equals the latest version saves nothing, as for
any manifest: it answers `unchanged`. When only files outside the agent
changed, such as `README.md`, it also notes `source_not_saved`: the new source
is not kept, and tracking Agents do not move.

| Call                                                    | Returns                                                                                                                                                                                                             |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.agentManifests.source(name, version)`           | [`GET /v1/agent-manifests/{name}/versions/{v}/source`](/reference/runtime/agents/manifests/get-the-folder-a-version-was-compiled-from): `{ compiler, files, bytes, entries }`, each entry `{ path, size, sha256 }`. |
| `client.agentManifests.sourceFile(name, version, path)` | [`GET …/versions/{v}/source/files/{path}`](/reference/runtime/agents/manifests/download-one-file-of-a-versions-folder): one file's bytes.                                                                           |
| `client.agentManifests.sourceDiff(name, from, to)`      | [`GET …/source/diff?from=&to=`](/reference/runtime/agents/manifests/compare-two-versions-folders): what changed between two versions' folders.                                                                      |

Studio shows a version's folder in the manifest's **Source** view, each file
marked when it was added or changed since the version before, and saves a
folder from the browser with **Upload folder…**, after showing its plan. See
[Studio](/docs/run/studio#the-tenant).

### Compare versions [#compare-versions]

```ts
const diff = await client.agentManifests.sourceDiff("triage", 2, 3);

for (const file of diff.added) console.log("+", file.path);
for (const file of diff.removed) console.log("-", file.path);
for (const file of diff.changed) console.log("~", file.path, file.from.size, "->", file.to.size);
console.log(diff.unchanged, "files unchanged");
```

`added` and `removed` list the files only `to` or only `from` holds,
`changed` the files both hold with other bytes, by SHA-256, each as `from`
held it and as `to` does, and `unchanged` counts the rest. Each list is
sorted by path. Either version saved as JSON is `404`, and a `from` or `to`
that is not a version number `400`.

## Convert [#convert]

| Command                                              | Writes                                                                                                                  |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `nylorun pull <name>[@<v>] [--out <dir>]`            | The folder a version was saved from, byte for byte. A version saved as JSON has none: it says to use `export --folder`. |
| `nylorun export --folder <name>[@<v>] [--out <dir>]` | The folder of a saved manifest, for a version saved as JSON or from TypeScript.                                         |
| `nylorun migrate folder <dir> [--out <dir>]`         | The folder of a [three-file](#three-files) folder, with its skills. Reads no Runtime.                                   |

Each writes to `--out`, by default `./<name>`, and never writes over a file
with other bytes. `pull` and `export` default to the latest version.
`export --folder` and `migrate folder` say whether the folder compiles back to
the same manifest, and note what they write differently. A saved manifest
names a skill's files by hash only, so `export` leaves them for you to copy
in; `migrate folder` reads them from the three-file folder, and moves each
`SKILL.md`'s `secrets` to `AGENT.md`.

```text
$ npx nylorun migrate folder agents/triage --out agents/triage-folder
Wrote 3 files to agents/triage-folder.
It compiles back to the same manifest. Check it with nylorun check agents/triage-folder, and save it with nylorun apply agents/triage-folder.
```

In code, `folderOfManifest(manifest)` from `@nylorun/agents` is the same
converter:

```ts
import { createClient, folderOfManifest } from "@nylorun/agents";

const client = await createClient();
const latest = await client.agentManifests.get("triage");

const { files, exact, notes } = folderOfManifest(latest.manifest);
// files: each path with its text or bytes; exact: compiles back to the same manifest
for (const note of notes) console.log(note);
```

## Save from code [#save-from-code]

```ts
import { checkFolder, createClient, diagnosticLine, packFolder } from "@nylorun/agents";

const folder = await packFolder("agents/triage");

// Offline, as nylorun check does.
const checked = await checkFolder(folder);
for (const diagnostic of checked.diagnostics) console.log(diagnosticLine(diagnostic));
if (!checked.manifest) process.exit(1);

const client = await createClient();
const plan = await client.agentManifests.planFolder(folder);
if (plan.errors.length > 0) throw new Error(plan.errors.join("\n"));

const saved = await client.agentManifests.saveFolder(folder, { ifMatch: plan.from ?? 0 });
await client.agents.put("teams/triage", { manifest: saved.manifest.name });
```

| Call                                                     | Does                                                                                                                                                                                                                              |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packFolder(dir)`                                        | Lists the folder's files with their sizes and SHA-256. Leaves out what `.nyloignore` names, skips `.git`, `.hg`, `.svn`, and `node_modules`, follows no symbolic link (`skipped`), and resolves [`extends`](#extends). Node only. |
| `checkFolder(folder)`                                    | Compiles it offline: `{ manifest?, diagnostics, compiler }`.                                                                                                                                                                      |
| `client.agentManifests.planFolder(folder, { ifMatch? })` | The [plan](/docs/run/agent-manifests#check-before-you-save), plus the compiled `manifest`, the `diagnostics`, and the `compiler`.                                                                                                 |
| `client.agentManifests.saveFolder(folder, { ifMatch? })` | Saves it, uploading the files the Runtime lacks.                                                                                                                                                                                  |
| `diagnosticLine(diagnostic)`                             | A diagnostic as `file:line:column severity: message`.                                                                                                                                                                             |
| `folderOfManifest(manifest)`                             | The folder of a manifest: `{ files, exact, notes }`. See [Convert](#convert).                                                                                                                                                     |
| `ignoreMatcher(text)`                                    | A matcher for `.nyloignore` text: `(path, directory?) => boolean`.                                                                                                                                                                |

`planFolder` and `saveFolder` compile the folder first, and throw
`FolderInvalidError`, with every diagnostic, before they send anything.

Any other client sends the folder by hash: each file with
`PUT /v1/files/sha256:<hex>`, then `PUT /v1/agent-manifests/{name}` with
`Content-Type: application/vnd.nylorun.folder+json` and the body
`{ "format": "nylorun.folder.v2", "entries": [{ "path": "AGENT.md", "size": 182, "sha256": "<hex>" }] }`,
every file listed.
`?dryRun=true` and `If-Match` work as for a JSON body. A file the Runtime does
not hold is `400 definition_files_missing`. The compiler is
`compileAgentFolder` in `@nylorun/core/folder`; such a client leaves out what
`.nyloignore` names and resolves `extends` itself.

## Three files [#three-files]

`agent.manifest.json`, with its `harness.manifest.json` and
`runtime.manifest.json`, is the three-file format, and it is deprecated.
`nylorun apply` and `composeManifest()` still read it, and warn:

```text
triage: warning: agent.manifest.json (the three-file format) is deprecated: nylorun migrate folder agents/triage writes it as an agent folder (AGENT.md), which saves the same manifest. It is still read until a later release.
```

`nylorun migrate folder` rewrites it as `AGENT.md` and its files: see
[Convert](#convert). The format itself is on
[Saving manifests](/docs/run/agent-manifests#three-files).

## Next step [#next-step]

<Cards>
  <Card title="Saving manifests" description="Versions, plans, and the Agents made from a manifest." href="/docs/run/agent-manifests" />

  <Card title="Agents" description="Make an Agent from the saved manifest and write its secrets." href="/docs/run/agents" />
</Cards>
