Types and errors
The whole surface on one page.
Every call
Five entry points. Everything else is a method on what you already hold.
// GETTING IN
AgentNava.getWorkspace(options?) -> Workspace // no request
ws.get() -> Workspace
ws.agents.create(fields) -> Agent
ws.agents.list() -> Agent[]
ws.projects.create(fields) -> Project
ws.projects.list() -> Project[]
ws.agent(agentId) -> Agent // no request
agent.conversation(conversationId) -> Conversation // no request
ws.project(projectId) -> Project // no request
ws.account(accountId) -> Account // no request; any account, the agent creator's or an end user's
ws.knowledgeBase(key) -> KnowledgeBase // no request
ws.endUser(endUserId) -> EndUser // no request
// APPS: every app the platform can connect, as a constant. A plain string works too.
App.GMAIL -> 'gmail' // App.GOOGLE_CALENDAR -> 'google-calendar'
// ACCOUNTS: one saved sign-in to one app. The agent creator's live in the workspace.
ws.accounts.connect({ app, nickname? }) -> { account, url } // send them to url
ws.accounts.list({ app? }) -> Account[] // each with usedBy: the agents that use it
ws.apps.tools(app) -> AppTool[] // an app's own tools: { id, name, summary, deletes? }
account.get() -> AccountData // a missing account throws account_not_found
account.waitUntilConnected({ timeoutMs? }) -> Account // connected, and not reconnecting
account.reconnect() -> { url } // every agent that names it follows
account.rename(nickname) -> Account
account.check() -> AccountData
account.disconnect() -> { agents } // always allowed; names the agents that lose it
// AN END USER'S OWN ACCOUNTS: each person signs in once, remembered for them
endUser.accounts.connect({ app }) -> { account, url }
endUser.accounts.list() -> Account[]
endUser.accounts.disconnectAll() -> { disconnected }
// WHAT THIS WORKSPACE HAS LEFT TO SPEND
ws.credits() -> { credits, standardTurnsRemaining, premiumTurnsRemaining }
ws.plan() -> SeatPlan | null
// AN AGENT
agent.start({ endUserId, version?, parameters?, attach?, secrets?, secretsExpireIn? }) -> Conversation
agent.update(fields) -> Agent // publishes a version
agent.get({ version? }) -> Agent
agent.versions() -> Agent[]
agent.delete() -> void
agent.conversations({ limit?, endUser?, before?, of? }) -> Conversation[] // of: 'customers' | 'team'
agent.setWorkflow(workflow) -> Agent // publishes a version
agent.removeWorkflow(name) -> Agent // publishes a version
agent.setTriggers(triggers) -> Agent // publishes a version
agent.rotateWebhookUrl() -> string // new webhook URL; the old one stops working
agent.webhookDeliveries({ limit? }) -> WebhookDelivery[] // newest first: accepted, refused, repeat, test
agent.sendTestWebhook() -> { conversationId } // runs the agent on a sample event; a real turn
agent.websiteChat.get() -> WebsiteChat // who can chat, the share link, the embed id
agent.websiteChat.access.get() -> 'restricted' | 'anyone' // new agents: 'restricted'
agent.websiteChat.access.set(access) -> 'restricted' | 'anyone' // 'anyone': anyone with the link
agent.websiteChat.allowedWebsites.get() -> string[] // websites a Restricted chat may be shown on
agent.websiteChat.allowedWebsites.set(list) -> string[] // replace it; a domain covers its subdomains
agent.knowledge.put(path, bytes) -> FileEntry
agent.knowledge.get(path) -> Uint8Array
agent.knowledge.text(path) -> string
agent.knowledge.list(dir?) -> FileEntry[]
agent.knowledge.remove(path) -> void
// CONNECTIONS: a name, an app, and whose account. Plain objects of the same shapes work too.
Connection.creatorAccount(app, account, { name?, tools? }) -> Connection // { name?, app, account: '<id>', tools? }
Connection.endUserAccount(app, { name?, tools? }) -> Connection // { name?, app, endUser: true, tools? }
agent.connections.add(connection) -> Agent // publishes a version
agent.connections.change(name, { account } | { endUser: true }) -> Agent // publishes a version
agent.connections.remove(name) -> Agent // publishes a version; the account stays
agent.connections.list() -> ConnectionStatus[]
agent.attach({ secrets }) -> Agent // secret values; accounts are never attached
// SECRETS: a value is written, never read back
ws.secrets.create({ name, value, agentId?, endUserId?, description?, allowedHosts? }) -> Secret // value: 8 characters or more
ws.secrets.list(agentId?) -> Secret[] // what exists, never the values
ws.secret(secretId) -> Secret // no request
secret.update(value) -> Secret // attached conversations use it on their next call
secret.delete() -> void // attached conversations stop using it at once
secret.setAllowedHosts(hosts) -> Secret // replaces the list of hosts it may be sent to; [] unpins
secret.allowedHosts -> string[] // as last read
endUser.secrets.create({ name, value, agentId?, description?, allowedHosts? }) -> Secret // this person's own
endUser.secrets.list() -> Secret[]
// A CONVERSATION, FROM AN AGENT OR A PROJECT
conversation.ask(text, { attachments?, secrets?, secretsExpireIn? }) -> Turn // secrets: fresh values, in force before the agent reads it
conversation.messages() -> Message[]
conversation.end() -> void
conversation.attach(secret | { secrets }, { expiresIn? }) -> Conversation // secrets only; a value lasts 1 hour by default
conversation.accountsNeeded() -> AccountNeeded[] // end user's accounts still missing, with links
conversation.files(dir?) -> FileEntry[]
conversation.readFile(path) -> string
conversation.readFileBytes(path) -> Uint8Array
conversation.writeFile(path, bytes) -> FileEntry
conversation.removeFile(path) -> void
conversation.fileUrl(path, { download?, ttl? }) -> string
// A PROJECT
project.start({ parameters? }) -> Conversation
project.update(fields) -> Project
project.get() -> Project
project.addAgent(agentId) -> Project
project.removeAgent(agentId) -> Project
project.archive() -> Project
project.delete() -> void
project.conversations() -> Conversation[]
project.files(dir?) -> FileEntry[]
project.readFile(path) -> string
project.fileUrl(path, { download?, ttl? }) -> string
// HELPERS
loadWorkflows(dir) -> Workflow[]ws.agent(id), ws.project(id) and ws.endUser(id) make no
network call. You get something you can call methods on, and the first request happens when you
call one. A returned object carries methods, so store the id and use ws.agent(id) to come back in.
Disconnecting an account
account.disconnect() signs the account out and removes it here. It is always
allowed, and it hands back the agents that used it. From their next turn those connections say
the account was disconnected, and none of them switches to another account in its place.
const [gmail] = await ws.accounts.list({ app: App.GMAIL });
const { agents } = await gmail.disconnect();
// [{ id: 'inbox-helper', agentName: 'Inbox helper', connectionName: 'work-gmail',
// published: true, inDraft: false }]Removing a connection from an agent is the other operation, and it never touches the
account: agent.connections.remove(name).
Providers remember that you already approved the application. Sign in again on the same
account with account.reconnect() and many of them skip the consent screen
entirely and reissue exactly what you had before, including anything the person declined the
first time.
This is not hypothetical. Google shows some permissions as separate checkboxes, so a person
can approve an app and leave half of it unticked. The account then reports
connected and looks healthy, and the gap only appears later as a 403 from the
part that was never granted. Disconnecting and connecting again is what brings the consent
screen back; then change the connections that should use the new account.
What is left to spend
A turn is charged against a prepaid balance, so this is the call that answers “can I still run something?”.
const { credits, standardTurnsRemaining, premiumTurnsRemaining } = await ws.credits();
const plan = await ws.plan(); // null when the workspace is on no seat planThe two turn counts are what the balance BUYS, which is the useful number: each grade has
its own price per turn, so the same balance is worth a different number of turns at each. On
plan(), seats: null means negotiated rather than unlimited, and
usdCentsPerMonth: null means there is no published price, which is a different fact
from a price of zero.
Types
type Workspace = { id: string; name: string; credits: number; createdAt: string };
type Agent = {
id: string;
name: string;
instructions: string;
greeting?: string;
grade: 'lite' | 'standard' | 'premium' | 'best';
filesystem: 'drive' | 'none';
tools: string[];
widgets: boolean;
workflows: Workflow[];
connections: AgentConnection[];
secrets: SecretRequirement[];
parameters: Parameter[];
triggers: Trigger[];
mcpServers: McpServer[];
httpTools: HttpTool[];
version: number;
createdAt: string;
updatedAt: string;
};
type Workflow = { name: string; when: string; content: string };
type Trigger =
| { kind: 'chat' }
| { kind: 'schedule'; cron: string; timezone?: string; message: string }
| { kind: 'interval'; seconds: number; maxRuns?: number; message: string }
| { kind: 'webhook'; message?: string; url?: string };
// One connection in the setup: a name, an app, and whose account. Exactly one of the two.
// tools: which of the app's own tools it may use; absent is the app's default, [] is none.
type AgentConnection =
| { name: string; app: AppId; account: string; tools?: string[] } // the agent creator's account, by id
| { name: string; app: AppId; endUser: true; tools?: string[] }; // each end user's own
// What you send: Connection.creatorAccount() / Connection.endUserAccount() build it.
type Connection =
| { name?: string; app: AppId; account: Account | string; tools?: string[] }
| { name?: string; app: AppId; endUser: true; tools?: string[] };
type AppId = (typeof App)[keyof typeof App] | (string & {}); // App.GMAIL is 'gmail'; any app id works
type SecretRequirement = { name: string; scope: Scope; required?: boolean }; // what an agent needs; a stored Secret holds the value
type Scope = 'fixed' | 'conversation'; // secrets only; 'conversation' comes only from the conversation
// One saved sign-in to one app.
type AccountData = {
id: string;
app: AppId;
label: string | null; // who it is signed in as, once someone has
nickname: string | null;
status: 'pending' | 'connected' | 'needs_sign_in' | 'error';
reconnecting: boolean; // a reconnect waits for the person; the account still works
owner: 'workspace' | 'end-user'; // every account call works on either
endUserId?: string;
connectedAt: string | null;
lastCheckedAt: string | null;
usedBy: UsedBy[]; // the agents that name it
};
type UsedBy = { id: string; agentId: string; agentName: string; connectionName: string; inDraft: boolean; published: boolean }; // id is Agent.id; agentId is the internal agt_ handle
type AccountNeeded = { name: string; app: AppId; url: string };
type Parameter = {
name: string;
type: 'string' | 'number' | 'date' | 'choice';
required?: boolean;
default?: string;
choices?: string[];
};
type McpServer = {
name: string;
url: string;
transport?: 'http' | 'sse';
headers?: Record<string, string>;
timeoutMs?: number;
};
type HttpTool = {
name: string;
description: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
url: string;
headers?: Record<string, string>;
body?: string;
params?: Record<string, HttpToolParam>;
secrets?: string[];
timeoutMs?: number;
};
type ConnectionStatus = { // from agent.connections.list()
name: string;
app: AppId;
account?: AccountData; // the agent creator's account, read fresh
endUser?: true; // each end user's own
status: string; // whether it works now
inDraft: boolean;
removedInDraft: boolean;
};
type AgentConversation = {
id: string; agentId: string; version: number;
startedAt: string; title?: string;
ready: boolean; requires: Requirement[];
};
type ProjectConversation = {
id: string; projectId: string;
startedAt: string; title?: string;
ready: boolean; requires: Requirement[];
};
type Project = {
id: string;
name: string;
goal: string | null;
managerInstructions: string | null;
grade: 'lite' | 'standard' | 'premium' | 'best';
status: 'active' | 'archived';
agents: ProjectAgent[];
createdAt: string;
updatedAt: string;
};
type ProjectAgent = { agentId: string; name: string; addedAt: string }; // an AGENT, not a person
type Requirement = { kind: 'secret' | 'connection'; name: string; required: boolean };
type Turn = Promise<string> & AsyncIterable<TurnEvent>;
type TurnEvent =
| { type: 'start'; turnId: string }
| { type: 'progress'; steps: Step[] }
| { type: 'tool'; name: string; state: 'start' | 'end'; label?: string; ok?: boolean }
| { type: 'text'; text: string }
| { type: 'file'; path: string }
| { type: 'widget'; widget: Widget }
| { type: 'credential_needed'; name: string; reason: 'missing' | 'expired' } // at the start of a turn
| { type: 'done'; reply: string; widgets?: Widget[] }
| { type: 'error'; message: string; retryable: boolean };
type Step = {
id: string;
label: string;
kind: 'read' | 'write' | 'edit' | 'search' | 'shell'
| 'web' | 'data' | 'task' | 'plan' | 'other';
status: 'active' | 'done' | 'error';
};
type Message = { role: 'user' | 'assistant'; content: string; at: string; widgets?: Widget[] };
type Widget = { surfaceId: string; at: number; components: WidgetPart[]; closed: boolean };
type WidgetPart = { id: string; component: string; [prop: string]: unknown }; // A2UI v0.9, AgentNava's set of parts
type Attachment = { kind: 'image' | 'file'; name?: string; mimeType: string; data: string };
type FileEntry = {
path: string;
kind: 'file' | 'directory';
mimeType: string;
size: number;
modifiedAt: string;
};
type AgentNavaError = {
code: string;
message: string;
status: number;
retryable: boolean;
retryAfter?: number;
};There are two kinds of conversation and they are two types. You know which one you hold, because you know which call returned it.
Errors
Every failure throws AgentNavaError.
import { AgentNavaError } from '@cerebro-labs/agentnava-sdk';
try {
await conversation.ask('...');
} catch (err) {
if (err instanceof AgentNavaError) {
err.code; // 'agent_not_found'
err.status; // 404
err.retryable; // false
err.retryAfter; // seconds, on 429
}
}status | |
|---|---|
| 400 | Something in the request was wrong |
| 401 | Missing or invalid key |
| 404 | It does not exist, or it is not in your workspace |
| 409 | Conflicts with current state, such as reusing an agent id |
| 429 | Rate limited. retryAfter says how long |
| 5xx | Ours. retryable is true |
One subclass carries extra fields: SecretHostNotAllowed (code
secret_host_not_allowed, 400), thrown when a save would send a key to a host it is not
pinned to. It adds secret, host and allowedHosts. See
Where a key may be sent.
404 covers both "no such thing" and "not yours", so a caller probing ids cannot tell another customer's agent from one that never existed. There is no 403.
Codes are stable. Messages are written for a person reading a log and will be reworded.
retryable is the only thing worth automating on: true for 5xx and 429, false for
everything else, because a 400 will fail identically forever.
When you iterate a turn instead of awaiting it, a failure arrives as an error
event and the loop ends.
Client options
AgentNava.getWorkspace();
AgentNava.getWorkspace({ apiKey, baseURL, timeout });
AgentNava.getWorkspace({ fetch: myFetch });apiKey | Defaults to AGENTNAVA_API_KEY |
baseURL | Defaults to AGENTNAVA_BASE_URL, then https://api.agentnava.com |
timeout | Per request, in milliseconds. Not applied to a streaming turn |
fetch | Your own fetch, for custom transport or authentication |
Construction is synchronous and makes no request. The SDK does not retry, cache or queue on your behalf: a call maps to a request, and what comes back is what the API returned.
Pointing at a different deployment
Two ways, and the explicit option wins over the environment variable.
// a dedicated endpoint
const ws = AgentNava.getWorkspace({ apiKey, baseURL: 'https://acme.agentnava.example' });# staging, which is where a test key works
AGENTNAVA_BASE_URL=https://api-staging.agentnava.comA trailing slash is fine, and a base with a path prefix works too, for an API gateway sitting in front of us.
Which reads like a bad key rather than the wrong host, and that is the wrong thing to go and
check. Staging is https://api-staging.agentnava.com. If you were
given a test key, set it.