Saving manifests
Save an agent and the runtime it needs as one named, versioned document, check it, and make Agents from it.
---
name: triage
description: Sorts new GitHub issues
---
Label and route new issues.{
"mcpServers": {
"github": {
"type": "streamable-http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
}
}
}These files are one agent manifest. Check it offline, save it, and make an Agent from it:
npx nylorun check agents/triage
npx nylorun apply agents/triage --dry-run
npx nylorun apply agents/triage
npx nylorun agents put users/alice/triage --manifest triage --owner aliceSaved, an agent manifest is one named, versioned document with two sections:
harness, the agent itself (what runs inside the agent loop), and runtime,
what the Runtime provides around it (the kind of sandbox, network, setup,
files, limits, secret slots, and triggers). Each save writes its next version.
An Agent is made from one version, and an Agent that
tracks the manifest moves to each new version in place.
The folder above compiles to this document, which --dry-run prints. The
${GITHUB_TOKEN} header becomes the server's credential and a bearer slot
bound to its URL, and with no runtime.json the runtime section is kind
none:
{
"manifestSchemaVersion": 8,
"name": "triage",
"description": "Sorts new GitHub issues",
"harness": {
"instructions": ["Label and route new issues."],
"mcpServers": {
"github": {
"name": "github",
"type": "streamable-http",
"url": "https://api.githubcopilot.com/mcp/",
"credential": "GITHUB_TOKEN"
}
}
},
"runtime": {
"kind": "none",
"secrets": {
"GITHUB_TOKEN": { "type": "bearer", "url": "https://api.githubcopilot.com/mcp/", "required": true }
}
}
}The Runtime compiles the folder itself, with the compiler check runs, and
keeps it byte for byte as the version's source. A manifest can also be saved
from code, or as one JSON document. The older
three-file format is deprecated.
A JSON document may start with a $schema URL for editors; clients drop it
before they save.
Save from code
client.agentManifests.save takes an agent you built and the runtime section
to run it in. This is what the starter's src/main.ts does:
import { Agent, createClient } from "@nylorun/agents";
const client = await createClient();
const triage = Agent({ id: "triage" })
.instructions("Label and route new issues.")
.build();
const { manifest, unchanged, warnings } = await client.agentManifests.save({
agent: triage,
runtime: { kind: "virtual", network: { allowedHosts: [] } },
});
await client.agents.put("users/alice/triage", { manifest: "triage", owner: "alice" });| Field | Meaning |
|---|---|
agent | A built agent or flow. Its manifest becomes the harness section. |
runtime | The runtime section. Without it, an Agent made from the manifest is kind none. |
name | The manifest's name. Default: the agent's id. |
description, metadata | Default: the agent's. |
harness | The harness that runs the loop (harness.id). Default nylorun. |
loop | The harness's own settings (harness.loop). |
memory | { access?, instructions? }: turns on the Agent's memory (harness.memory). |
save also takes the document itself, such as a parsed JSON file, or what
composeManifest read from a three-file folder. An agent
folder is saved with saveFolder(await packFolder(dir)); see
Agent manifests. agentManifest(input)
checks any of them locally, against the schema the Runtime enforces, and
answers the document with manifestSchemaVersion filled in.
The SDK refuses before it sends anything: an agent with a tool that runs your
code (tool({ run })), since the Runtime runs agents from their manifests
alone; a tool's approval, since the Tenant's
tool policy decides which calls wait; and a
requires the runtime section misses (ManifestGapsError). It
uploads the skill and runtime files the Runtime does not hold, then saves.
save answers the version saved, or the latest with unchanged: true when
nothing differs, and the warnings of the save. Each version records its
hash, when it was saved, who saved it (savedBy), and how many agents
were made from it. A save moves every Agent that tracks the manifest: see
Versions. Request and response shapes are on
Save an agent manifest.
Fields
| Field | Meaning |
|---|---|
$schema | For editors: https://docs.nylorun.com/schemas/agent-manifest/v8.json. |
manifestSchemaVersion | 8. |
name | 1 to 64 letters, digits, ., _, and -, starting with a letter or digit. Model budgets of scope manifest and tool policy rules name it. |
description | Up to 1024 characters by default. |
metadata | Your own JSON. |
harness | The agent. See below. |
runtime | What the Runtime provides. See below. Leave it out for kind none. |
The harness section is what you author with the
Agent builder, or by hand:
harness field | Meaning |
|---|---|
id | The harness that runs the loop. Default nylorun, ours. Another needs runtime.kind: "pod" with an image. See Harness. |
instructions | See Instructions. |
tools | HTTP tools and agents used as tools. See Tools. |
skills | Each skill may list the secrets slots its scripts read. See Skills. |
mcpServers | Each server may name the credential slot it authenticates with. See MCP. |
plugins | The plugins folded in. See Plugins. |
outputSchema | The JSON Schema of a structured answer. |
loop | The harness's own settings, passed to it with each turn. |
memory | { access, instructions }. See Agent memory. |
A flow's harness section is kind: "workflow" with root and agents
instead; it always runs in our harness. See Flows.
The runtime section:
runtime field | Meaning |
|---|---|
kind | Required. virtual, pod, or none. See Kinds. |
image | Pods: the workload image. Default the Tenant's, else python:3.13-slim. |
resources | { cpus, memory }, such as { cpus: 2, memory: "2GiB" }. Default 2 CPUs and 1024MiB, at most the Tenant's limits. |
storage | Pods: the volume's size, default 5GiB. |
network.allowedHosts | Hosts the sandbox may reach, within the Tenant's network limit. No egress without it. Pods also take *.example.com patterns. |
lifecycle | { idle, deleteAfter }. See Lifecycle. |
setup, files | Pods: prepare a new disk. See Setup and input files. |
limits | artifacts (fileBytes, totalBytes), skills (totalBytes), and disk (versionBytes, versionEntries): lower than the Tenant's limits for Agents made from this manifest. See Runtime limits. |
secrets | The secret slots an Agent made from it needs. |
triggers | What starts its turns with no client connected. See Triggers and wakes. |
selfWake | true lets the agent book its session's next turn. See Triggers and wakes. |
Private networks, loopback, the host, and cloud metadata endpoints are always
blocked, whatever allowedHosts says.
Sections that fit
The Runtime checks the two sections against each other when a manifest is
saved, and refuses every gap at once with 422 manifest_inconsistent, each in
details.gaps:
- A custom harness (
harness.idother thannylorun) needsruntime.kind: "pod"with animage. - An MCP server's or HTTP tool's
credentialnames abearerorheadersslot ofruntime.secretsbound to its exact URL. - A skill's
secretsnameenvironment_secretorenvironment_variableslots. - Skills need a sandbox:
virtualorpod.
A gap reads like
harness.mcpServers.github.credential "GITHUB_TOKEN" is not a secret slot in runtime.secrets.
A server that names no credential still matches a secret by its URL. A save
is also refused (400) when it breaks the Tenant's limits or names files the
Runtime does not hold (definition_files_missing), and a skill whose files
hold more than the skill limit is 413 limit_exceeded.
Requires
requires is a contract a harness carries for any runtime it is combined
with. It never reaches the Runtime: .requires({ sandbox, secrets }) on the
Agent builder and a harness file's requires are checked
by the SDK, which throws ManifestGapsError and sends nothing, and by
nylorun apply, which saves nothing and exits 1:
requires | The runtime section needs |
|---|---|
sandbox: "any" | kind virtual or pod. |
sandbox: "pod" | kind pod. |
secrets: [...] | Each name declared in secrets. |
Check before you save
plan() saves nothing and answers what a save would do. planFolder() does
the same for a manifest's files, and adds the compiled manifest and the
compiler's diagnostics. nylorun apply --dry-run and
PUT /v1/agent-manifests/{name}?dryRun=true do the same.
import { readFile } from "node:fs/promises";
const document = JSON.parse(await readFile("manifests/triage.json", "utf8"));
const plan = await client.agentManifests.plan(document);
if (plan.errors.length > 0) throw new Error(plan.errors.join("\n"));
console.log(`v${plan.from} -> v${plan.to}`, plan.changes, plan.warnings, plan.moves, plan.behind);| Field | Meaning |
|---|---|
from, to | The latest version (null for a new manifest) and the version a save would write. |
unchanged | true when nothing differs from the latest. |
changes | Each changed field: { section, path, op, from, to }, section being manifest, harness, or runtime. |
errors | Every reason the save would be refused: the gaps between the sections first, then the Tenant's limits and missing files. |
warnings | { code, message }, never a refusal: image_tag (an image tag rather than a digest), pods_unavailable (kind pod with no sandbox pods), agents_move, agents_behind. |
moves | The tracking Agents a save moves: now, at their pod's next start (nextStart), or once their required secrets are set (secrets). |
behind | The Agents that stay on an older version: pinned ones, and tracking ones whose kind, harness, or storage the version changes. |
A plan uploads the files the manifest names, since files are stored by their content.
Save under If-Match
Pass the version you read as ifMatch, 0 for a manifest that does not exist
yet. When someone saved since, the save is refused with 412 manifest_changed:
read the manifest again and retry.
const latest = await client.agentManifests.get("triage");
await client.agentManifests.save(
{ ...latest.manifest, description: "Sorts new GitHub issues" },
{ ifMatch: latest.version },
);The HTTP header is If-Match: <version>, and the CLI flag
--if-match <n>. nylorun apply saves under If-Match set to the version it
planned against.
Versions and references
An Agent names its manifest when it is made, and again with each put:
manifest | The Agent |
|---|---|
"triage" | Tracks the manifest: runs its latest version now, and moves with each save. |
"triage@3" | Runs version 3 and stays there. |
| Call | Does |
|---|---|
client.agentManifests.get(name) | The latest version. get(name, 3) reads version 3. |
client.agentManifests.list() | Each manifest's latest version. |
client.agentManifests.versions(name) | Every version, newest first, with who saved it and how many Agents were made from each. |
client.agentManifests.delete(name) | Delete every version. Refused with 409 manifest_in_use while an Agent was made from one. |
client.agentManifests.source(name, version) | The folder a version was saved from. A version saved as JSON has none (404). |
client.agentManifests.sourceDiff(name, from, to) | The files added, removed, and changed between two versions' folders. |
A version saved from files
has a source: its compiler, files, and bytes. Its files can be read
back one by one, compared with another version's (see
Compare versions), or written
back to disk with nylorun pull <name>[@<n>].
Reading a manifest takes agents:read, agents:write, or
agent-manifests:write. Acting for a person, saving and deleting take
agent-manifests:write. A trusted issuer's token reaches no manifest.
Secret slots
runtime.secrets declares what an Agent made from the manifest needs: each
secret's name, its type, its binding, whether it is required, and a
description. A manifest never holds a value.
type | Binding |
|---|---|
bearer | url, via?, identity? |
headers | url, via?, identity? |
environment_secret | allowedHosts (1 to 32 hosts by default), inject? |
environment_variable | None |
The Agent's view lists each slot in slots, with required and whether it is
set. Write a value with { value } (or { headers } for a headers slot):
the binding comes from the slot, so app code cannot widen where a key goes. A
name the manifest does not declare is 409 secret_not_in_manifest. See
Secret slots.
await client.agents.secrets.put("users/alice/triage", "GITHUB_TOKEN", {
value: process.env.ALICE_GITHUB_TOKEN!,
});Setup and input files
A pod's disk can start prepared. files copies files in, then setup runs its
commands in order:
const requirements = await client.artifacts.upload("pandas==2.2.3\n", {
name: "requirements.txt",
});
await client.agentManifests.save({
name: "analyst",
harness: { instructions: ["Analyse the data you are given with pandas."] },
runtime: {
kind: "pod",
image: "python:3.13-slim",
network: { allowedHosts: ["pypi.org", "files.pythonhosted.org"] },
files: [
{
artifactId: requirements.artifact.artifactId,
version: requirements.version.version,
path: "/workspace/requirements.txt",
},
],
setup: [
{ command: "python -m venv .venv && .venv/bin/pip install -r requirements.txt", timeout: "15m" },
],
},
});
await client.agents.put("projects/acme", { manifest: "analyst" });| Field | Meaning |
|---|---|
files[] | { artifactId, version, path }: a version of a Tenant-wide artifact. A file artifact is written at path; a folder artifact is unpacked under it. Or { path, files }: files by hash. path is /workspace or below. |
setup[] | { command, cwd?, timeout? }, run with /bin/sh -c. cwd defaults to /workspace; timeout defaults to 10m, at most 30m by default. |
At most 16 of each by default; the Runtime limits environmentSetupCommands,
environmentFiles, and setupCommandMs set these bounds (see
Runtime limits). They run once per new disk (created or
reset), before the first turn, with the Agent's variables, secrets, and
egress. A resume keeps what they made. A failed command fails the pod and the
turn waiting for it, with an agent.failed event that carries its exit code
and the end of its output; the next turn start runs the setup again. A version
that changes setup or files applies at the pod's next start, which runs
the setup again on the same volume. A virtual Agent refuses both with 400.
Files by hash
A files entry can name files by content hash instead of an artifact version,
so the manifest moves to another installation with its files.
runtimeFiles(path, contents) hashes them, and save uploads those the
Runtime lacks:
import { runtimeFiles } from "@nylorun/agents";
const kit = await runtimeFiles("/workspace/kit", {
"bin/run.sh": "#!/bin/sh\necho ready\n",
});
await client.agentManifests.save(
{
name: "analyst",
harness: { instructions: ["Analyse the data you are given."] },
runtime: {
kind: "pod",
files: [kit.entry], // { path: "/workspace/kit", files: { "bin/run.sh": "sha256:…" } }
setup: [{ command: "sh kit/bin/run.sh" }],
},
},
{ files: kit.bytes },
);Each file is copied under path at its relative path, before setup runs. A
file holds at most the Tenant's skill limit (2 GiB by default), and an entry
names at most 1000 files by default (environmentFilesPerEntry). A file the
Tenant does not hold is refused when the manifest is saved
(definition_files_missing). In a runtime file,
{ "path": "/workspace/kit", "from": "./kit" } names a folder beside the file,
which nylorun apply hashes and uploads the same way.
Three files
Deprecated
The three-file format is deprecated. Write an
agent manifest instead:
npx nylorun migrate folder <dir> converts one, offline.
Before AGENT.md, a manifest written as files was three JSON files, one
folder per agent, so that many agents share one runtime file. nylorun apply
and composeManifest() still read them, keep no source, and warn on each,
naming nylorun migrate folder:
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.{
"$schema": "https://docs.nylorun.com/schemas/agent-manifest/v8.json",
"manifestSchemaVersion": 8,
"name": "triage",
"description": "Sorts new GitHub issues",
"harness": "./harness.manifest.json",
"runtime": "../../shared/virtual.runtime.json"
}{
"$schema": "https://docs.nylorun.com/schemas/harness-manifest/v1.json",
"instructions": ["Label and route new issues."],
"mcpServers": {
"github": {
"name": "github",
"type": "streamable-http",
"url": "https://api.githubcopilot.com/mcp/"
}
},
"credentials": { "github": "GITHUB_TOKEN" },
"requires": { "sandbox": "any" }
}{
"$schema": "https://docs.nylorun.com/schemas/runtime-manifest/v1.json",
"kind": "virtual",
"network": { "allowedHosts": [] },
"secrets": {
"GITHUB_TOKEN": { "type": "bearer", "url": "https://api.githubcopilot.com/mcp/", "required": true }
}
}| File | Holds |
|---|---|
agent.manifest.json | The name, description, and metadata, with harness and runtime as paths (or inline). |
harness.manifest.json | The harness section. skills may be a folder of skill folders (each SKILL.md may list its secrets), mcpServers a path to an mcp.json, and plugins plugin folders. credentials maps an MCP server or HTTP tool to its slot; requires is checked against the runtime; memory turns on agent memory. |
runtime.manifest.json | The runtime section. A files entry may be { path, from }: a folder beside the file. |
Each path is relative to the file that names it. The two section files have
their own schemas, https://docs.nylorun.com/schemas/harness-manifest/v1.json
and https://docs.nylorun.com/schemas/runtime-manifest/v1.json.
nylorun apply <dir> composes the folder into one document, prints the plan
(the version it moves from and to, each change, warnings, and errors), and
saves under If-Match, uploading the files the Runtime does not hold. Errors,
gaps between the sections included, save nothing and exit 1. A folder with
neither an agent.manifest.json nor an AGENT.md applies each subfolder that
has one, so npx nylorun apply agents/ saves every agent, AGENT.md and
three-file folders alike; every folder composes or compiles before anything is
sent. From code, composeManifest does the same:
import { composeManifest } from "@nylorun/agents";
const composed = await composeManifest("agents/triage");
await client.agentManifests.save(composed);composed.gaps lists what the Runtime would refuse and what requires misses;
save refuses those with ManifestGapsError. A file that cannot be read or
does not validate throws ComposeError. To write the three files from an
agent saved before agent manifests, see
Compatibility. A shared runtime file has a folder
equivalent: extends in runtime.json.
CLI
| Command | Does |
|---|---|
nylorun init <path> [--example] | Write <path>/AGENT.md, a new agent manifest named after the path's last segment. --example adds an HTTP tool, mcp.json, and a trigger. |
nylorun check [<dir>...] [--fix] [--json] | Compile each agent manifest offline and print every problem by file, line, and column. --fix makes the fixable edits; --json is for CI. |
nylorun apply [<dir>...] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]] | Plan and save each folder's manifest. --dry-run prints the plans, and the manifest compiled from each folder, and saves nothing; --if-match <n> saves only over version n (one folder); --agent then makes or moves the Agent; --watch plans again on each change, and --save saves it. |
nylorun chat <agent> [--session <id>] [--message <text>] | Talk to an Agent: send each line, print each reply. |
nylorun export --folder <name>[@<n>] [--out <dir>] | Write a saved manifest out as files, for one saved from JSON or TypeScript. |
nylorun pull <name>[@<n>] [--out <dir>] | Write the folder a version was saved from, byte for byte. |
nylorun migrate folder <dir> [--out <dir>] | Rewrite a three-file folder as AGENT.md and its files, offline. |
nylorun agent-manifests list [--json] | Each manifest's latest version: its runtime kind, harness, and the Agents made from it. |
nylorun agent-manifests get <name>[@<n>] | The latest version, or version n, as JSON. |
nylorun agent-manifests versions <name> | Its versions, with the Agents made from each. |
nylorun agent-manifests rm <name> | Delete every version. |
nylorun agents put <id> --manifest <name[@n]> | Make an Agent from it, tracking or pinned. |
nylorun agents list --outdated | The Agents moving to another version of their manifest. |
See CLI.
In the starter
npm create @nylorun/agent@beta writes runtime/scratch.runtime.json, a
virtual workspace with no network:
{
"$schema": "https://docs.nylorun.com/schemas/runtime-manifest/v1.json",
"kind": "virtual",
"network": { "allowedHosts": [] }
}src/main.ts saves each agent in agents/index.ts as an agent manifest with
that runtime section, and makes the Agent its sessions run on, named after the
agent, each time npm run dev or npm start runs it:
import { readFile } from "node:fs/promises";
import { createClient } from "@nylorun/agents";
import { agents } from "../agents/index.js";
const client = await createClient();
const { $schema: _, ...runtime } = JSON.parse(
await readFile(new URL("../runtime/scratch.runtime.json", import.meta.url), "utf8"),
);
for (const agent of agents) {
const { manifest } = await client.agentManifests.save({ agent, runtime });
const made = await client.agents.put(agent.id, { manifest: agent.id });
console.log(`Saved agent ${agent.id} v${manifest.version} (Agent ${made.id} runs v${made.resolvedVersion})`);
}The Agent tracks its manifest, so each save moves it, and each session's next turn runs your edit. See Your project.
Studio
The Tenant page's Manifests tab lists the manifests in one table: each one's name and description, latest version, the kind of Agent it makes, how many Agents were made from it, and when it was saved. A version saved from files is marked beside its number. A manifest opens beside the list, with a version picker, the Agents made from the version, and two views: Spec, the version's harness and runtime sections as a readable spec, and Source, the folder it was saved from, each file marked when it was added or changed since the version before. Its ⋯ menu makes an Agent pinned to the version (New Agent from vN) or shows the manifest as JSON.
Studio does not edit a manifest: edit its folder in its repository and save
it with nylorun apply, or with Upload folder…, which picks a folder in
the browser, shows the plan with the compiler's diagnostics, and saves it
under If-Match. See Studio.