Read first · Where this runs
Your agents/<agent-name>/ folder lives on your computer — markdown files you edit, version-control, and grep. It's the local source-of-truth for the agent's spec.
The deploy script (agents/<agent-name>.ts) reads those files and calls an.specs.push(loadAgentDir(...)) to upload them to AgentNava, creating a new spec version on the backend. an.agents.configure assembles that spec into an agent version; deployToTest / deployToProd spins the agent up on AgentNava infrastructure.
The agent never runs on your machine. Once a version is pushed and deployed, your local files are no longer the runtime source — the pushed version is. To change behavior: edit locally → re-run the deploy script → redeploy. List or fetch what's been pushed with an.specs.list({ key }) / an.specs.get('spec_…@v2'), or browse them at console.agentnava.com → Specs.
How a spec becomes a running agent
Your machine · local
Agent folder: agents/<agent-name>/
AGENT.md + skills/ + workflows/ + commands/ + (optional) subagents/ and knowledge/. You edit and version-control these.
↓ your deploy script runs
@agentnava/kit · uploads
an.specs.push(loadAgentDir(...))
Reads your folder, uploads each file, and stamps a new spec version on the backend (e.g. spec_4f1c9a@v2).
↓ spec version pinned
@agentnava/kit · configure
an.agents.configure({ spec, knowledge, connections, ... })
Assembles the pushed spec + KB + integrations + secrets into a new agent version (e.g. ver_3c5f). Idempotent on key.
↓ deploy
AgentNava platform · runtime
deployToTest(...) or deployToProd(...)
The agent now runs on AgentNava infrastructure at <agent-name>.agents.agentnava.com. End users connect through sessions; tools and skills execute server-side.
The five object types
| Object | What it is | How to create it |
| Spec |
A bundle of behavior files — AGENT.md plus any skills, subagents, workflows, and slash commands. Same layout as Claude Code's .claude/. |
an.specs.push({ key, instructions, skills, subagents, workflows, commands }) or an.specs.push(loadAgentDir('./agents/foo')). Each push stamps a new spec version. |
| Knowledge |
RAG documents — .md, .pdf, .txt, images. Agents AND every workflow/subagent always read from the full base on every turn. |
an.knowledge.upload({ key, files }). Versioned per upload. See Knowledge. |
| Connections |
Pre-authorized integrations (Slack, Gmail, GitHub, MCP servers, …) — set up once in the workspace, referenced by key. Each connection registers tools. |
Dashboard, or connect.slack({ key: 'slack-acme' }) in code. Full catalog at Connections. |
| Secrets |
Workspace-level API keys / env values injected into tool handlers. |
Dashboard, or an.secrets.set('ZILLOW_API_KEY', '…'). Referenced from agents by name. |
| Plugins |
Pre-packaged bundles of skills, subagents, commands, and MCP servers. Claude Code plugins (.claude-plugin/plugin.json) install unchanged. |
an.plugins.install('github:owner/repo#tag') or local path. Reference by name in configure({ plugins }). |
| Widgets |
A typed event protocol for rendering anything in your UI. The agent emits ctx.emitWidget({ kind, props }) from a tool handler; your UI dispatches on kind and renders. No built-in catalog — every widget kind is yours. |
From inside a tool: ctx.emitWidget({ kind, props }). See Widgets for the full protocol. |
File kinds inside a Spec
The Spec is a folder of markdown files. Five kinds — all first-class, all use the same frontmatter conventions Claude Code uses, so skills and subagents you've already built drop in unchanged.
AGENT.md
instructions
Required
The agent's core instructions — what it does, how it talks, what it won't do. The CLAUDE.md equivalent. Always sent to the model. One per spec.
# agents/concierge/AGENT.md
# BayHomes Concierge
You help prospective buyers explore listings.
Address the user as "you". Keep replies under two paragraphs unless asked.
Never quote a final price — always defer to a human agent for pricing.
skills/<name>/SKILL.md
auto-invoked
Optional
A focused, auto-invokable module the agent loads when the description matches the user's request. YAML frontmatter is the same as Claude Code skills — name, description, allowed-tools, model. Drop existing Claude Code skills in unchanged.
# agents/concierge/skills/format-listing/SKILL.md
---
name: format-listing
description: Format a property listing card with price, beds, sqft, and one-line pitch
allowed-tools: [Read, lookup_zestimate]
---
When showing a listing, render…
The platform also bundles built-in capabilities (file ops, web search, structured-output helpers, etc.) that work without any declaration.
workflows/<name>.md
multi-step playbook
Optional
A multi-step playbook the agent runs end-to-end — e.g. "morning digest", "weekly report", "intake → qualify → handoff". Frontmatter declares the trigger (chat-keyword, schedule, webhook) and required connections. Every workflow has full access to the agent's Knowledge base and Connections — no extra wiring.
# agents/concierge/workflows/morning-digest.md
---
name: morning-digest
description: Daily 7am email summarizing new listings + price changes
trigger: { kind: schedule, cron: '0 7 * * 1-5' }
requires: [gmail, hubspot]
---
1. Query HubSpot for active buyers…
2. Match against new MLS listings…
3. Draft + send digest emails…
commands/<name>.md
user-invokable
Optional
A slash command the end user types in chat (/reset, /refresh-kb, /handoff). Same format as Claude Code commands — argument-hint, allowed-tools, optional model.
# agents/concierge/commands/handoff.md
---
description: Hand the conversation off to a human agent
argument-hint: "[reason]"
---
Tell the user a human will follow up about $ARGUMENTS, then emit a handoff event.
On every push the entire bundle is uploaded. specs.push stamps a new version only if content actually changed.
Custom subagents — advanced
The runtime delegates side-tasks to a fresh-context worker automatically. You don't configure this, and most agents never need to think about it. Built-in safety: max delegation depth is bounded, recursion is rejected, and per-session cost is capped.
The one reason to declare a named subagent is when you want a predictable, repeatable specialist — a compliance reviewer, a sales-tone checker, a structured research role. Drop a markdown file at subagents/<name>.md using the same format as Claude Code subagents (.claude/agents/*). It's purely additive.
# agents/concierge/subagents/researcher.md
---
name: researcher
description: Looks up market comparables and neighborhood data
tools: [WebSearch, WebFetch, lookup_zestimate]
model: premium
---
You research recent comparable sales…
Import from Claude Code
Existing Claude Code work is directly portable. No converter, no edits — the spec bundle uses the same file layout and frontmatter conventions Claude Code uses.
Skills + commands — copy into your local spec folder
Your agents/<agent-name>/ directory is the local source-of-truth that gets uploaded on the next specs.push (see Where this runs). Copy existing Claude Code files into it:
cp -r ~/projects/my-tool/.claude/skills/* agents/<agent-name>/skills/
cp -r ~/projects/my-tool/.claude/commands/* agents/<agent-name>/commands/
On the next an.specs.push(loadAgentDir(...)) the files are uploaded to AgentNava as part of a new spec version; after configure + deploy, the agent running on AgentNava infrastructure picks them up. Everything works as-is:
- Skills — YAML frontmatter (
name, description, allowed-tools, model, disable-model-invocation) is interpreted identically.
- Commands —
$ARGUMENTS substitution, argument-hint, and allowed-tools are honored. Users invoke them with /name in chat.
Subagents (.claude/agents/*) port the same way but are an advanced concept — most agents don't need them. See Custom subagents.
The one Claude-Code-specific concept we don't carry over is CLAUDE.md — we call its equivalent AGENT.md instead. If you're porting a Claude Code project, rename CLAUDE.md → AGENT.md and you're done.
Plugins — install once, attach by name
Claude Code plugins (.claude-plugin/plugin.json bundles of skills, subagents, commands, hooks, and MCP servers) install at the workspace and are referenced from any agent:
// 1) Install at the workspace (one-time, idempotent on tag).
await an.plugins.install('github:owner/repo#v1');
// → { name: 'pr-helper', version: 'v1', skills: 3, subagents: 1, mcpServers: 1 }
// 2) Reference by name from configure.
await an.agents.configure({
// ...
plugins: ['pr-helper'],
});
Install sources we accept:
github:owner/repo, github:owner/repo#v1, github:owner/repo#commit-sha
https://example.com/plugin.tgz (signed tarball)
./vendor/my-plugin (local path)
Once installed, the plugin's bundled skills, subagents, commands, hooks, and MCP servers all become available on every agent that lists it.
MCP servers
Any MCP-compatible server is added as a connection. Three transports supported, same as Claude Code's .mcp.json:
import { connect } from '@agentnava/kit';
await an.agents.configure({
// ...
connections: [
// HTTP / SSE — most managed MCP services
connect.mcp({ name: 'linear', url: 'https://mcp.linear.app/sse' }),
// With a bearer token from your workspace secrets
connect.mcp({
name: 'jira',
url: 'https://mcp.example.com',
headers: { Authorization: `Bearer ${process.env.JIRA_MCP_TOKEN}` },
}),
// Stdio — for local-process MCP servers
connect.mcp({
name: 'local-fs',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'],
}),
],
});
Every tool the MCP server exposes becomes a callable tool for the agent — no wrapping, no conversion. To see what's available, an.connections.tools('linear') returns the live tool list.
The lifecycle in TypeScript
The whole flow is a single file. Each step is an SDK call.
// agents/concierge.ts
import { AgentNava, connect, loadAgentDir } from '@agentnava/kit';
const an = new AgentNava(); // reads process.env.AGENTNAVA_API_KEY
// 1) Push the Spec bundle (AGENT.md + skills + subagents + workflows + commands).
// loadAgentDir reads the folder and infers each file's kind from its path.
const spec = await an.specs.push(
loadAgentDir('./agents/concierge', { key: 'concierge' }),
);
// → { id: 'spec_4f1c9a', version: 2 }
// 2) Upload the Knowledge base (.md, .pdf, .txt, images).
const kb = await an.knowledge.upload({
key: 'bayhomes-handbook',
files: ['./knowledge/handbook.pdf', './knowledge/policies.md', './knowledge/floorplan.png'],
});
// → { id: 'kb_8b07', version: 3 }
// 3) Configure → returns a new agent version that pins to spec@v2 and kb@v3.
const v = await an.agents.configure({
key: 'bayhomes-concierge',
name: 'BayHomes Concierge',
modelClass: 'standard',
spec, // bundle from step 1
knowledge: [kb], // agents AND workflows always see this
triggers: [{ kind: 'chat' }],
connections: [connect.slack({ key: 'slack-bayhomes' })],
secrets: ['ZILLOW_API_KEY'],
});
// → { agentId: 'agt_2b8e', versionId: 'ver_3c5f' }
// 4) (Optional) deploy to test. Dev-only URL.
const test = await an.agents.deployToTest(v.versionId);
console.log('test:', test.url);
// 5) Deploy to prod when you're happy.
const prod = await an.agents.deployToProd(v.versionId);
console.log('prod:', prod.url);
Re-running the file is safe and idempotent. specs.push only stamps a new version if content changed. agents.configure upserts on key. deployToTest/deployToProd always produce a new instance.
Explicit fields, if you prefer
loadAgentDir is a convenience over the explicit form. Both are valid:
const spec = await an.specs.push({
key: 'concierge',
instructions: readFileSync('./agents/concierge/AGENT.md', 'utf8'),
skills: [
{ name: 'format-listing', content: readFileSync('./agents/concierge/skills/format-listing/SKILL.md', 'utf8') },
],
workflows: [
{ name: 'morning-digest', content: readFileSync('./agents/concierge/workflows/morning-digest.md', 'utf8') },
],
commands: [
{ name: 'handoff', content: readFileSync('./agents/concierge/commands/handoff.md', 'utf8') },
],
// subagents: [...] // advanced — see Custom subagents below
});
What configure takes
Every field below is a property of the object you pass to an.agents.configure(...).
key
string
Required
A dev-chosen, stable identifier for this agent in your workspace. The backend uses it to upsert — first configure with a new key creates the agent (and assigns an opaque agt_… ID); later calls with the same key update the existing one.
key: 'bayhomes-concierge'
You control key. The backend controls agentId. Use key in source code; use agentId for sessions, deploys, and rollbacks at runtime.
name
string
Required
What users address the agent as in chat (e.g. "Hey Concierge, …") and how it introduces itself. Also appears in the workspace header, embed widget, and catalog cards — but the conversational identity is what matters.
name: 'BayHomes Concierge'
spec
Spec | string
Required
The Spec object you got from an.specs.push(...), or its ID + version as a string ('spec_4f1c9a@v2'). The agent version pins to whichever spec version you pass — re-pushing the spec doesn't change deployed agents.
spec // pass the object directly
// or
spec: 'spec_4f1c9a@v2'
// or
spec: 'spec_4f1c9a' // resolves to latest at configure time, then pinned
knowledge
(KnowledgeBase | string)[]
Optional
One or more Knowledge bases for RAG (.md, .pdf, .txt, images). The runtime indexes them and retrieves relevant chunks on every turn. The agent and every workflow, subagent, and skill it spawns always have full access — you do not need to re-declare it anywhere. Pin to specific KB versions the same way as Spec.
knowledge: [kb, 'kb_legal@v5']
End users (or you, post-deploy) can also upload files into a Knowledge base — see Knowledge.
modelClass
'standard' | 'premium' | 'advanced'
Required
How much reasoning the agent applies on each turn. Higher classes think longer, can chain more tool calls before replying, and cost more.
| Class | When to pick it | Default route |
standard | Short replies, FAQ, simple Q&A. One shot, at most one tool call. | GLM-4.7 tier |
premium | Drafts, multi-step reasoning, RAG across several sources, summarization with citations. | Sonnet tier |
advanced | Planning across many tools and steps — research, code generation, complex workflows. | Opus tier |
Pin a snapshot if you want behavior frozen across model upgrades:
modelClass: 'standard' // rolling — auto-upgrades over time
modelClass: 'standard@2026-05' // pinned to the May 2026 snapshot
modelClass: 'premium@v3' // pinned to integer release v3
provider
'auto' | 'bedrock' | 'azure-openai' | 'vertex' | 'openrouter'
Optional
default: 'auto'
Whose account pays for the model calls. 'auto' bills the managed runtime. Any other value routes inference through your own cloud account — tokens billed there. See Operate → BYOK.
triggers
Trigger[]
Required
When the agent wakes up. Without at least one trigger, the agent never runs.
triggers: [
{ kind: 'chat' },
{ kind: 'webhook' },
{ kind: 'schedule', cron: '0 7 * * 1-5' },
{ kind: 'loop', everySeconds: 600, maxRuns: 24 },
{ kind: 'manual' },
]
| Kind | The agent runs when |
chat | A user sends a message via the embed widget, hosted chat page, or your own UI. |
webhook | An external service POSTs to the agent's URL — Stripe events, GitHub webhooks, your own backend. |
schedule | A cron expression matches. Daily standup digests, hourly polls, weekly reports. |
loop | A self-driving loop runs the agent every N seconds, optionally capped. Useful for monitoring tasks. |
manual | Your code calls an.agents.run(agentId, { ... }). Useful for batch jobs. |
connections
ConnectionRef[]
Optional
Pre-authorized OAuth integrations the agent can use during a turn. Each connection registers a handful of tools (Slack registers post_message, list_channels, etc.) and may add a trigger. Set up the OAuth once in the workspace dashboard, then reference by key from code. Full catalog at Connections.
connections: [
connect.slack({ key: 'slack-acme' }),
connect.gmail({ key: 'gmail-support' }),
connect.mcp({ url: 'https://mcp.linear.app/sse' }),
]
secrets
string[]
Optional
Workspace secret names that should be injected into tool handlers as environment variables. Set the actual values once via the dashboard or an.secrets.set(name, value) — they never appear in your TS source.
secrets: ['ZILLOW_API_KEY', 'STRIPE_KEY']
// in a tool handler:
defineTool({
// ...
handler: async ({ address }, ctx) => {
const r = await fetch(`https://zillow…`, {
headers: { 'X-Key': ctx.secrets.ZILLOW_API_KEY },
});
},
});
tools
ToolDef[]
Optional
Custom functions you write in TypeScript. The agent decides when to call them. Use this for behavior no connection covers — calling your internal API, computing something, or hitting a SaaS the catalog doesn't yet support.
import { defineTool, t } from '@agentnava/kit';
const lookupZestimate = defineTool({
name: 'lookup_zestimate',
description: 'Get the Zillow zestimate for an address',
input: t.object({ address: t.string() }),
handler: async ({ address }, ctx) => {
// ctx.secrets, ctx.sessionId, ctx.user available here
},
});
tools: [lookupZestimate]
plugins
PluginRef[]
Optional
Pre-packaged bundles of skills, subagents, commands, and MCP servers — installed once at the workspace and referenced by name. Claude Code plugins (.claude-plugin/plugin.json) install unchanged: an.plugins.install('github:owner/repo#v1'), then list them here.
plugins: ['claude-code-review', 'pr-helper@v2']
hooks
Hook[]
Optional
Event hooks fired by the runtime around the agent loop. Same event names and shape as Claude Code hooks — PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart. Use them for audit logging, PII scrubbing, or gating risky tool calls.
hooks: [
{ event: 'PreToolUse', match: { tool: 'Bash' }, run: async (ctx) => { /* approve or block */ } },
{ event: 'PostToolUse', match: { tool: '*' }, run: async (ctx) => { /* log */ } },
]
allowedTools
string[]
Optional
Whitelist of built-in tools the agent (and its subagents) can call. If unset, every built-in is available. Built-ins mirror Claude Code: Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch, Task, TodoWrite.
allowedTools: ['Read', 'WebSearch', 'Task']
// disallowedTools also accepted
permissionMode
'default' | 'acceptEdits' | 'plan' | 'bypass'
Optional
default: 'default'
How aggressively the agent is allowed to act without confirmation. Same semantics as Claude Code's permission modes — default asks for risky tool calls, acceptEdits auto-approves file edits, plan only proposes, bypass never asks. Surface a UI affordance to your users if you change this from default.
meta
Record<string, unknown>
Optional
Anything you set here is readable from tool handlers via ctx.meta.X. Use it for per-deploy config the agent doesn't reason about — brand colors, deploy targets, feature flags.
meta: {
deployTarget: 'embed',
brandColor: '#B91C1C',
region: 'eu-west',
}
Event stream
The agent runtime emits a typed SSE stream that any host UI can consume. The same shape comes back whether you're hitting a test or a prod deploy.
type AgentEvent =
| { type: 'message-start'; messageId: string }
| { type: 'message-delta'; messageId: string; delta: string }
| { type: 'message-end'; messageId: string; final: string }
| { type: 'tool-start'; toolId: string; name: string; input: unknown }
| { type: 'tool-end'; toolId: string; ok: boolean; result?: unknown; error?: string }
| { type: 'phase'; label: string; state: 'start' | 'end' }
| { type: 'error'; message: string }
| { type: 'done' };