NylorunDocsBeta
BuildDefine

Agent manifests

Write an agent or a flow as a folder of Markdown and JSON, check it offline, and save it with nylorun apply.

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?"
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). 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 support from the manifest, and nylorun chat talks to it. Sessions run the saved manifest, never the files.

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.

The loop

StepCommandDoes
Writenylorun 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.
Checknylorun 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; --json is for CI.
Save and runnylorun 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.
Talknylorun 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 in .vscode/settings.json, each only when it is missing:

$ 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:

$ 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, 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:

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.

The commands act on the project's local Tenant, or on the Runtime that NYLORUN_RUNTIME_URL and NYLORUN_SERVER_KEY name. See CLI.

Layout

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, flows, and plugins are folders of the same format inside the folder.

AGENT.md

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.
KeyTypeDefaultManifest field
nameRequired. 1 to 64 letters, digits, ., _, and -, starting with a letter or digit.name
descriptionRequired. A non-empty string. At most the Tenant's manifestDescriptionChars (1024 by default), checked at save.description
format1.1None: the compiler's format.
harnessA 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.nylorunharness.id
inputSchemaA flow's input: a path to a JSON Schema file, or the schema inline. A flow only; elsewhere it is input_schema_refused. See Flows.Noneharness.inputSchema
outputSchemaA path to a JSON Schema file, or the schema inline.Noneharness.outputSchema
mcpServersThis agent's settings for each server of mcp.json. See MCP servers.Noneharness.mcpServers.<server>.deferred, .tools
skillsThis agent's settings for each skill of skills/. See Skills.Noneharness.skills.<name>.secrets
memory{ access, instructions }: access is read_write or read_only, instructions at most 4096 characters. See Agent memory.Offharness.memory
loopA 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.Noneharness.loop
metadataA map of your own.Nonemetadata

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. A flow's AGENT.md has frontmatter only: see Flows.

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

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. It also takes $schema (see Editor schemas) and 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, a trigger, or files/, compiles with { "kind": "none" } plus those.

extends

agents/analyst/runtime.json
{
  "$schema": "https://docs.nylorun.com/schemas/agent-folder/v1/runtime.json",
  "extends": "../../platform/pod.runtime.json",
  "resources": { "memory": "4GiB" }
}
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

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 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).

Server keyValue
typeRequired. streamable-http or sse.
urlRequired. The server's URL.
headersA 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.

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":

HeaderIn the manifestSlot
"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 writtennone

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:

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

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

KeyValue
deferredtrue or false defers every tool of the server, or none. See Deferred tools.
toolsSettings per tool, keyed by the server's own tool names, "*" for the rest: enabled and deferred. See Per-tool settings.
credentialA plugin's server only: the secret slot it authenticates with. A server in mcp.json names its credential in a header instead. See plugins/.

A name that mcp.json does not declare is an error. Which calls wait for a person is the Tenant's 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/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 named after its file: 1 to 64 letters, digits, _, and -, with no __.

KeyTypeDefault
descriptionWhat the tool does, for the model.None
inputSchemaRequired. A JSON Schema, inline or a path to a schema file.
outputSchemaA JSON Schema, inline or a path. A JSON answer that does not match fails the call.None
httpRequired. The request.
http keyValueDefault
urlRequired. The URL, whose path may name input fields as {field}. See The request.
methodGET, POST, PUT, PATCH, or DELETE.POST
credentialA secret slot bound to url. See Secret slots.None
timeoutMsMilliseconds, at most the Tenant's Runtime limit 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). 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:

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, 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>/.

triggers/

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 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.

KeyValue
scheduleA five-field cron, quoted.
timezoneAn IANA time zone, with schedule only. Default UTC.
atOne run, at an RFC 3339 time with an offset, such as "2026-11-01T09:00:00+05:30".
webhookstandard-webhooks. See Webhooks.
sessionRequired. 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/open-pr/
├── SKILL.md
└── scripts/open.sh

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

KeyValue
nameRequired. 1 to 64 lowercase letters, digits, and single hyphens, equal to the folder's name.
descriptionRequired. At most 1024 characters.
license, compatibility, metadata, allowed-toolsOptional. 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:

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:

$ 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.

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.

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):

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

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:

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

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:

{ "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.

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) offers an agent used as a tool. It runs on the session's Agent, in its runtime. See 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 subagentRefused as
runtime.json, triggers/, files/nested_refused: they are the outer folder's.
format, harness, memory, or loop in its AGENT.mdnested_refused: it runs in the outer agent's harness.
agents/ of its ownsubagent_depth: agents used as tools nest one level deep.
A credential on its MCP servers, or secrets on its skillscredential_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

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

NodeRunsBuilder
{ "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
{ "chain": [ … ] }Each node in order, each one's output the next one's input.Pipe
{ "switch": { "cases": { … }, "default": … } }The one case the previous stage's output names.Switch
{ "parallel": { "<key>": … } }Each named branch at once, on the same input.Parallel
{ "map": { "each": … } }The node once per item of the previous stage's output.Map
{ "loop": { "run": …, "verify": …, "max": <n> } }run, then verify, until it passes or max runs. verify is { "agent": "<id>" } or { "tool": "<name>" }.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.

Plugins

plugins/<name>/ holds an Agent Plugin 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.

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
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."
}
$ 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
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

FolderAgent manifest v8
AGENT.md name, description, metadataname, description, metadata
AGENT.md harnessharness.id
AGENT.md body, then instructions/*.mdharness.instructions, one string per file
AGENT.md outputSchema, memory, loopharness.outputSchema, harness.memory, harness.loop
AGENT.md inputSchema (a flow)harness.inputSchema
mcp.json, with AGENT.md mcpServersharness.mcpServers, credential from ${NAME}
skills/<name>/, with AGENT.md skillsharness.skills.<name>, with files by hash and secrets
tools/<name>.jsonharness.tools[], an HTTP tool named <name>
agents/<id>/, with no flow.jsonharness.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 extendsruntime
A derived slotruntime.secrets.<name>
triggers/<id>.mdruntime.triggers.<id>, with the body as prompt
files/**runtime.files[], { path: "/workspace", files }
Anything elseNothing; 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

An agent

writer/
├── AGENT.md
├── instructions/01-style.md
├── output.schema.json
└── README.md
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.
writer/instructions/01-style.md
Use the present tense. No marketing adjectives.
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

orders/
├── AGENT.md
├── schemas/order.json
└── tools/get_order.json
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, 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:

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

triage/
├── AGENT.md
├── mcp.json
└── runtime.json
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. runtime.json declares the slot itself, with a description, so nothing is derived:

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.

Triggers

digest/
├── AGENT.md
├── mcp.json
├── triggers/nightly.md
└── triggers/alerts.md
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.
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.

A flow

brief/
├── AGENT.md
├── flow.json
└── agents/
    ├── researcher/AGENT.md
    └── writer/AGENT.md
brief/AGENT.md
---
name: brief
description: Researches a topic, then writes a short brief on it.
---
brief/flow.json
{ "chain": [{ "agent": "researcher" }, { "agent": "writer" }] }
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.
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.
$ 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 in the nylorun/agents repository: a switch, a map, a parallel stage, a loop, and an HTTP tool stage, in a sub-flow:

repo-brief-folder/flow.json
{
  "chain": [
    { "agent": "triage" },
    { "id": "route", "switch": {
        "cases": { "repo": { "agent": "research" } },
        "default": { "agent": "decline" } } }
  ]
}
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" }
  ]
}
$ 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

support/
├── AGENT.md
└── agents/billing/AGENT.md
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.
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

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

$ 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).

SeverityMeans
errorThe folder does not compile, and nothing is sent.
warningThe folder compiles. Read it before you save. See Warnings.
infoSomething the compiler added, such as a derived slot (slot_derived).
noteA 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:

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

CodeWhen
agent_missing, frontmatter_missingNo AGENT.md at the root, or one without frontmatter.
name_missing, name_invalid, name_mismatch, description_missingname or description missing or invalid, or name other than the name saved under.
unknown_keyA key the file does not take, with "Did you mean" or the known keys.
value_invalid, harness_invalidA frontmatter value of the wrong type, such as metadata that is not a map, or a bad harness id.
json_invalid, frontmatter_invalid, not_utf8A file that does not parse: JSON comments, trailing commas, a key twice, YAML aliases or tags.
wrong_name, wrong_syntaxagent.md in lower case, or runtime.yaml, mcp.yaml, runtime.jsonc, and the like.
reserved_directoryA file tools/, triggers/, instructions/, skills/, or schemas/ does not take, such as tools/notes.txt.
path_invalid, path_case, path_duplicate, path_refusedA path with .., \, control characters, or not in NFC; two paths that differ only in case; .git/ or node_modules/.
file_missingA file the folder lists whose bytes the compiler was not given.
mcp_type, mcp_invalidA server with no type, stdio, ws, or http, or with no url.
credential_placement, credential_default, credential_name, credential_twice, credential_urlsA ${NAME} the compiler cannot bind to a slot. See Credentials.
server_unknown, skill_unknownAGENT.md names a server or skill the folder does not have.
skill_invalid, skill_secretsA skill without SKILL.md, with a bad name or description, or with secrets.
tool_invalidA tool file with name or agent, without http or inputSchema, or with a bad file name.
approval_removedapproval in a tool file or an MCP tool's settings. See tools/.
schema_missing, schema_invalid, path_outsideA 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_invalidA schema that is not valid JSON Schema 2020-12, or uses what the engine does not read. See Schema files.
schedule_invalid, timezone_invalidA cron that does not parse, or an unknown time zone.
trigger_twice, prompt_emptyA trigger in both triggers/ and runtime.json, or a trigger with an empty body.
manifest_inconsistentSections that do not fit, at the field: skills with no sandbox, a custom harness off a pod, a credential bound to another URL.
runtime_kindfiles/ off a pod, or network on kind none.
flow_invalid, flow_body, flow_refused, flow_namespace, flow_cycleA 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.
agent_unknown, agent_name, tool_unknown, tool_unused, tool_twice, input_schema_refusedA 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_unsupportedWhat a subagent or a flow's agent does not take. See Subagents.
plugin_invalid, plugin_duplicateA plugin without a valid plugin.json or mcp.json $schema, or a skill or server name the agent already has. See Plugins.
format_unsupportedA format other than 1.
not_supportedAn unresolved extends: resolve it before uploading. See extends.
limit_exceededOver a limit.

Warnings

A warning never stops a save.

CodeWhen
secret_file.env or .env.* anywhere, or a .pem or .key file outside skills/ and files/.
secret_literalA literal value in an authentication header, or a token-shaped string in any header. See Credentials.
tool_policy_fileA root tool-policy.json, policy.json, or *.policy.json. Tool policy is the Tenant's; the file is only kept with the source.
schema_urlA $schema that names another kind of file, or a schema that is not ours. See Editor schemas.
json_schema_keywordA JSON Schema keyword close to a known one, such as proprties.
loop_keyA loop key the nylorun harness does not read.
instructions_emptyAn agent with no instructions.
agent_unusedAn agent of a flow's agents/ that no flow.json names. It is left out of the manifest.
plugin_unknown_keyA 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.

$ 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.

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: 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).

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.

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:

$ 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
$ 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

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

$ 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

LimitDefaultRuntime limit
Files in a folder2,000folderFiles
Bytes in a folder50 MiBfolderBytes
One Markdown or JSON file the compiler reads1 MiBfolderTextFileBytes
A path1 to 1024 charactersFixed

The first three are the Tenant's Runtime limits, 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

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.

CallReturns
client.agentManifests.source(name, version)GET /v1/agent-manifests/{name}/versions/{v}/source: { compiler, files, bytes, entries }, each entry { path, size, sha256 }.
client.agentManifests.sourceFile(name, version, path)GET …/versions/{v}/source/files/{path}: one file's bytes.
client.agentManifests.sourceDiff(name, from, to)GET …/source/diff?from=&to=: 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.

Compare versions

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

CommandWrites
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 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.

$ 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:

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

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 });
CallDoes
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. Node only.
checkFolder(folder)Compiles it offline: { manifest?, diagnostics, compiler }.
client.agentManifests.planFolder(folder, { ifMatch? })The plan, 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.
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

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:

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. The format itself is on Saving manifests.

Next step

On this page