AgentNava · Build

Agents

One object describes the agent. There is no separate spec and no deploy step.

What an agent is

An agent is one object. There is no separate spec, no manifest and no deploy step: the fields below are the agent, and it is usable the moment you create it.

const agent = await ws.agents.create({
  name: 'Refund checker',
  instructions: 'You help a support agent decide whether a refund is warranted.',
});
Two fields is a real agent

Everything else has a default. Add workflows when it needs procedures, connections when it needs an account, knowledge when it needs to read something. Not before.

In the portal it reads Live

An agent shows in the portal as Live the moment it exists: it answers your calls, your team's tests and its chat page now, and there is nothing to switch on. That is true however it was made, from code or in the portal. Who can use its website chat is a separate setting, Restricted to your team until you open it, and you never need it to use the agent from code.

Fields

namestringrequired

What people call it. Shown wherever the agent appears.

instructionsstringrequired

Who the agent is, what it is for, and how it should answer. This is the system prompt.

idstringoptionalgenerated

Your own stable id. Unique in your workspace. Omit it and we mint one.

greetingstringoptional

What the agent says before anyone types. Omit it and the agent waits. Costs nothing: no model runs.

guardrails{ stayOnTopic?: boolean; rules?: string[]; checkMessagesFirst?: boolean }optional{ stayOnTopic: true, checkMessagesFirst: true }

What the platform enforces, even when someone tries to talk the agent out of it. See Guardrails.

grade'lite' | 'standard' | 'premium' | 'best'optionalstandard

Which model the agent thinks with, and what each turn costs. Standard, the default, is the fastest and costs 0.02 credits a turn; lite costs 0.01, premium 0.05 (it thinks harder, more slowly) and best 0.20, where one credit is one US dollar. The old name fast still works and means standard.

filesystem'drive' | 'none'optionaldrive

drive gives every conversation a durable filesystem. none means the agent acts only through its tools, which suits an agent whose whole job is calling them.

workflowsWorkflow[]optional[]

Named procedures. See Workflows.

connectionsConnection[]optional[]

The apps it uses. Each is a name, an app, and whose account: Connection.creatorAccount(app, account) for the agent creator's account, or Connection.endUserAccount(app) for each end user's own. See Connections.

secretsSecret[]optional[]

Keys it needs.

parametersParameter[]optional[]

Typed values collected when a conversation starts.

triggersTrigger[]optional[{ kind: 'chat' }]

What starts a run. See Triggers.

toolsstring[]optional[]

Platform capabilities to switch on, such as 'web_search'.

widgetsbooleanoptionaltrue

Whether the agent answers with cards, tables and charts where the chat can show them, such as the website chat. Set false for an agent that should answer in text only. In the builder this is the Widgets switch under Version, and the Agent Builder can set it when asked.

mcpServersMcpServer[]optional[]

MCP servers whose tools it may call. See Tools.

httpToolsHttpTool[]optional[]

Your own endpoints, described so the agent can call them.

Changing an agent

Updating publishes a new version. Fields you leave out keep their current value, so a wording change is one field.

await agent.update({
  instructions: '... Be strict about perishable goods.',
});

Conversations already running keep the version they started on. New ones get the new version. That is what makes it safe to edit an agent while people are talking to it.

await agent.versions();            // the agent at each version
await agent.get({ version: 3 });   // as it was
Change thisReaches a conversation already open
update, setWorkflow, removeWorkflowNo. Each publishes a version
knowledge.put, knowledge.removeYes, on the next turn
connections.add, connections.change, connections.removeNo. Each publishes a version
An account signed in again, or disconnectedYes, on the next turn
attach({ secrets })Yes, for scope: 'fixed'
A stored secret replaced or deleted (update, delete)Yes, on the next tool call, in every conversation it is attached to
setTriggersNot applicable. A trigger starts a new run
The rule behind that table

A version pins a reference. Instructions and workflows are the thing, so a version freezes them. Knowledge is a folder, and a connection points at an account, so the version pins which one and what is on the other end stays live. Correcting a policy document reaches a conversation in flight; rewording an instruction does not.

Guardrails

An agent's instructions are what it tries to do. Its guardrails are what the platform enforces, so a visitor cannot argue the agent past them.

await agent.update({
  guardrails: {
    stayOnTopic: true,
    rules: [
      'Never offer discounts or change prices.',
      'Never send email to anyone outside @example.com.',
    ],
  },
});

stayOnTopic is on unless you set it to false. It declines anything outside the agent's job, judged from its own instructions: other work like code or homework, "ignore your instructions", requests for its prompt or files, and "I'm the owner" claims made to get special treatment.

Each rule is one plain-English sentence, at most 20 rules. Every rule is checked in three places:

  • The message. A message asking the agent to break a rule is declined. The agent never reads it and replies politely in its own voice instead.
  • Each action. A tool call that breaks a rule, an email to the wrong address for example, does not happen. The agent is told and explains.
  • The reply. A reply that breaks a rule is taken back and answered again within it.

The message is checked before the agent reads it, which adds about 0.2 seconds, so a message that breaks a guardrail never reaches the agent and nothing it asked for is done. The check reads the whole message, the agent's instructions and workflows, and the conversation so far, so a follow-up like "make it cheaper" is judged in context and a request one of its workflows covers is not called off-topic. Set checkMessagesFirst: false to answer at once instead: the agent then reads every message, and its actions and replies are still checked. Replies are checked as they stream, so they cost no wait.

When a guardrail fires, the turn's stream says so with a guardrail event naming the rule, and a reply that was taken back arrives as a reset before the new words.

Guardrails are part of the configuration, so changing them publishes a version, and a conversation already in progress keeps the version it began on. In the web app they are the Guardrails tab of the Build screen; checkMessagesFirst is set from the SDK only.

Website chat

Every agent has a chat page of its own, the share link, and an embed script that puts it on your website. Who can chat is one setting, in Google Docs' words:

Who can chatThe share linkOther websites
Restricted (every new agent)Members of your workspace, signed in to AgentNavaOnly the allowed websites below
Anyone with the linkAnyoneAny website

Restricted is how a team tests an agent: copy the link, send it to a teammate, and they sign in and chat. Members are not held to the daily message limit a visitor has. Open it to anyone when the agent is ready for the public; each visitor's turns are charged to your workspace.

await agent.websiteChat.access.get();          // 'restricted' for a new agent
await agent.websiteChat.access.set('anyone');  // anyone with the link, on any website

const chat = await agent.websiteChat.get();
chat.chatUrl;       // the share link, to send or put in an iframe
chat.embedAgentId;  // what the embed script's data-agent-id takes
chat.access;        // who can chat

Publishing a new version moves the website chat to it, as it moves new conversations. An agent that needs each user's own account cannot have a website chat, because a visitor has none to bring: its page serves nobody, and a version that adds such a need leaves the page on the last version it could serve. Pausing an agent stops it everywhere, its website chat included.

Allowed websites. While the agent is Restricted, its chat appears inside another website, through the embed script, only on the websites you list, and visitors there chat without signing in. A listed domain covers its subdomains, and an empty list means no other website. The same setting and list are in the portal, in the Website chat panel on the agent's page and in the Publish dialog.

How strong each setting is: Restricted with no websites is enforced by our servers for every request. Restricted with websites relies on the website the visitor's browser reports, which a script can fake, like CORS. Anyone with the link is open.

await agent.websiteChat.allowedWebsites.set(['https://acme.com/help', 'localhost']);
// ['acme.com', 'localhost']: each entry becomes its domain; localhost is for testing
await agent.websiteChat.allowedWebsites.get();

Coming back to an agent

You will not hold the object between requests. Store the id and pick it up again:

const agent   = ws.agent(row.agentId);   // no network call
const conversation = await agent.start({ endUserId: 'u_ananya' });

ws.agent(id) makes no request. You get something you can call methods on, and the first request happens when you call one.