NylorunDocsBeta
RunAgents

Saving manifests

Save an agent and the runtime it needs as one named, versioned document, check it, and make Agents from it.

agents/triage/AGENT.md
---
name: triage
description: Sorts new GitHub issues
---
Label and route new issues.
agents/triage/mcp.json
{
  "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 alice

Saved, 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" });
FieldMeaning
agentA built agent or flow. Its manifest becomes the harness section.
runtimeThe runtime section. Without it, an Agent made from the manifest is kind none.
nameThe manifest's name. Default: the agent's id.
description, metadataDefault: the agent's.
harnessThe harness that runs the loop (harness.id). Default nylorun.
loopThe 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

FieldMeaning
$schemaFor editors: https://docs.nylorun.com/schemas/agent-manifest/v8.json.
manifestSchemaVersion8.
name1 to 64 letters, digits, ., _, and -, starting with a letter or digit. Model budgets of scope manifest and tool policy rules name it.
descriptionUp to 1024 characters by default.
metadataYour own JSON.
harnessThe agent. See below.
runtimeWhat 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 fieldMeaning
idThe harness that runs the loop. Default nylorun, ours. Another needs runtime.kind: "pod" with an image. See Harness.
instructionsSee Instructions.
toolsHTTP tools and agents used as tools. See Tools.
skillsEach skill may list the secrets slots its scripts read. See Skills.
mcpServersEach server may name the credential slot it authenticates with. See MCP.
pluginsThe plugins folded in. See Plugins.
outputSchemaThe JSON Schema of a structured answer.
loopThe 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 fieldMeaning
kindRequired. virtual, pod, or none. See Kinds.
imagePods: 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.
storagePods: the volume's size, default 5GiB.
network.allowedHostsHosts 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, filesPods: prepare a new disk. See Setup and input files.
limitsartifacts (fileBytes, totalBytes), skills (totalBytes), and disk (versionBytes, versionEntries): lower than the Tenant's limits for Agents made from this manifest. See Runtime limits.
secretsThe secret slots an Agent made from it needs.
triggersWhat starts its turns with no client connected. See Triggers and wakes.
selfWaketrue 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.id other than nylorun) needs runtime.kind: "pod" with an image.
  • An MCP server's or HTTP tool's credential names a bearer or headers slot of runtime.secrets bound to its exact URL.
  • A skill's secrets name environment_secret or environment_variable slots.
  • Skills need a sandbox: virtual or pod.

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:

requiresThe 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);
FieldMeaning
from, toThe latest version (null for a new manifest) and the version a save would write.
unchangedtrue when nothing differs from the latest.
changesEach changed field: { section, path, op, from, to }, section being manifest, harness, or runtime.
errorsEvery 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.
movesThe tracking Agents a save moves: now, at their pod's next start (nextStart), or once their required secrets are set (secrets).
behindThe 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:

manifestThe Agent
"triage"Tracks the manifest: runs its latest version now, and moves with each save.
"triage@3"Runs version 3 and stays there.
CallDoes
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.

typeBinding
bearerurl, via?, identity?
headersurl, via?, identity?
environment_secretallowedHosts (1 to 32 hosts by default), inject?
environment_variableNone

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" });
FieldMeaning
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.
agents/triage/agent.manifest.json
{
  "$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"
}
agents/triage/harness.manifest.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" }
}
shared/virtual.runtime.json
{
  "$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 }
  }
}
FileHolds
agent.manifest.jsonThe name, description, and metadata, with harness and runtime as paths (or inline).
harness.manifest.jsonThe 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.jsonThe 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

CommandDoes
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 --outdatedThe 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:

runtime/scratch.runtime.json
{
  "$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:

src/main.ts
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.

Next step

On this page