/docs / app-sdk

App SDK

The App SDK connects an app's page to Nebo. It is one script that Nebo serves to every app, and it puts a single global on the page, NeboAppSDK. There is nothing to install or build.

Load the SDK

<script src="/sdk/nebo.global.js"></script>
<script>
  const { nebo, identity, storage, agents, janus, decide, chat, surfaces } = NeboAppSDK;
</script>

The global is NeboAppSDK; there is no nebo global. NeboAppSDK.nebo is the SDK instance, and every module is also exported at the top level, so NeboAppSDK.storage and NeboAppSDK.nebo.storage are the same object. To avoid shadowing the browser, nebo.fetch is exported as neboFetch and nebo.WebSocket as NeboWebSocket.

identity

CallReturns
identity.get(){id, name, displayName, description, persona, model, skills, inputValues} for the app’s employee. Cached.
identity.invalidate()Clears the cache.

storage

A key-value store for the app. It needs storage:readwrite. It is the same store the app's employee reads and changes: a record the owner adds by talking to the employee shows on the page, and one typed into the page is one the employee can find.

const deals = (await NeboAppSDK.storage.getItem('deals')) ?? [];
deals.push({ name: 'Example renewal', closes: '2026-10-31' });
await NeboAppSDK.storage.setItem('deals', deals);
CallBehavior
storage.getItem(key)Exactly what setItem stored (object, list, number or string), or null.
storage.setItem(key, value)Stores any JSON value.
storage.removeItem(key)Removes one key; it no longer appears in keys().
storage.keys()All keys.
storage.clear()Removes every key.
storage.onChange(handler)Calls handler({appId, keys, action, source}) after every write, by the employee (source: "employee") or by any open window of the app, this one included (source: "page"). Returns a function that stops listening.

Redraw when the data changes, whoever changed it:

async function load() {
  render((await NeboAppSDK.storage.getItem('deals')) ?? []);
}
load();
const stop = NeboAppSDK.storage.onChange((change) => {
  // { appId, keys: ['deals'], action: 'set' | 'delete', source: 'employee' | 'page' }
  if (change.keys.includes('deals')) load();
});

Only the app's own employee reaches this store, and only for its own app; another employee asks the app's employee. Pick keys both sides can find (one key holding a list, or one key per record under a prefix such as deal:42) and describe them in the employee's instructions. A string that is itself valid JSON, such as "42", comes back parsed, so wrap it in an object if the type matters. Keep each value well under 2 MB; setItem does not throw when a write is refused.

agents

Ask the app's employee, or another employee, and get its answer. Asking another employee needs subagent:<employee-id>.

const { text } = await NeboAppSDK.agents.invoke('Which open deals close this month?');

for await (const part of NeboAppSDK.agents.stream('Summarize the pipeline.')) {
  output.textContent += part.text;
}
CallReturns
agents.invoke(message, {agent, data}){text, tools} when the employee finishes.
agents.stream(message, {agent, data})An async iterator of {text, done}.

janus

A direct model call with no persona, memory or tools.

CallReturns
janus.complete({messages, model, temperature, max_tokens, system})The reply text as a string.
janus.stream(same)An async iterator of text chunks.

decide

Typed decisions: ask named questions about some data and get each answer back with probabilities and a confidence, in one fast call. Nothing is written as text, so use it for judgments (is this lead hot, which category does this ticket belong to, how urgent is it) and keep counting, dates and thresholds in your own code. The app's employee can ask the same questions with its decide tool, for example over the app's stored records.

const { answers } = await NeboAppSDK.decide({
  state: { company: 'Example Co', status: 'asked for a quote today' },
  questions: {
    tier: {
      type: 'choice',
      instructions: 'How warm is this lead, judging by `status`?',
      criteria: { hot: 'ready to buy', warm: 'interested', cold: 'not now', other: "can't tell" }
    },
    fit: {
      type: 'score',
      instructions: 'How well does `company` fit our customers?',
      criteria: ['poor', 'fair', 'good', 'great']
    },
    reply: { type: 'noul', instructions: '`status` asks us for a reply.' }
  }
});

if (answers.tier.choice === 'hot' && answers.tier.confidence > 0.8) flag(lead);
// answers.fit.score is fractional: 0 is "poor", 1.5 is between "fair" and "good".
// answers.reply.noul is the probability the statement holds, from 0 to 1.
Question typecriteriaAnswer
choiceAn object of 2 to 255 named options, {option: description}. Add an escape option such as other when the list is not complete.choice (the option picked), confidence (0 to 1) and probabilities per option.
scoreA list of 2 to 10 ordered levels, lowest first.score (fractional, 0 is the first level), confidence and probabilities per level.
noulNone: the question is one statement to judge.noul, the probability the statement holds. No separate confidence.
  • decide({state, questions}) resolves to {model, answers, usage}, with each answer under the name you gave its question.
  • state is text or any JSON. Keep it to the fields the questions need and name those fields in backticks inside instructions. The whole question lives in instructions; the question's name only labels the answer. Very long state is shortened in the middle before it is sent.
  • It throws an error whose message is the reason. The error has no status code, so compare the message if you need to tell the cases apart:
    • A malformed question, for example a choice with one option (the bot answers 400, with what is wrong).
    • No work left on the owner's account: "You've used all the work included in your account. Choose a plan or add credits to continue." (429). Retrying does not help until the owner adds a plan or credits.
    • Too many decisions at once: "Too many decisions at once. Try again in a moment." (429).
    • The bot is not signed in: "Decisions need NeboAI connected. Sign in to NeboAI and try again." (503).
    • The decision service failed (502).
  • Decisions are billed to the bot owner's NeboAI account like any model call.See pricing.

neboFetch

// Relative path: the app's own sidecar.
const res = await NeboAppSDK.neboFetch('/deals?stage=open');

// Absolute URL: through Nebo's proxy. Needs network:api.example.com.
const rates = await NeboAppSDK.neboFetch('https://api.example.com/rates');

A relative path goes to the app's sidecar at /apps/<id>/api/<path>. An absolute http or https URL goes through Nebo's proxy and needs network:<host> or network:*.

NeboWebSocket

new NeboAppSDK.NeboWebSocket() opens a live socket to the app's employee and reconnects with backoff. It takes no arguments and has send(data), close(), onopen, onmessage, onerror and onclose.

surfaces and a2ui

surfaces.connect() opens the app's live channel. Today it carries interactive cards from the app's employee: call a2ui.init(processor) with a MessageProcessor from @a2ui/web_core, which your page bundles (the SDK has no renderer of its own). A card reaches only the app it was made for, a click on it goes to that app's employee, and a page opened after a card was sent does not receive it, so keep what the page must always show in storage.

The SDK also defines typed events such as text_content, state_delta and surface_update, with surfaces.on(type, handler), surfaces.send(name, payload) and surfaces.state. Nebo does not send those events to app pages yet, and nothing answers send. For live data, use storage.onChange.

chat

Embed Nebo's chat with the app's employee in the page.

NeboAppSDK.chat.mount(document.getElementById('chat'), {
  placeholder: 'Ask about a deal',
  contextId: 'deal-42'
});
NeboAppSDK.chat.setContext({ deal: 'Example renewal' });
CallBehavior
chat.mount(element, options)Options: placeholder, theme, height, borderless, contextId, scope.
chat.send(text)Sends a message.
chat.setContext(context)Sets the context the employee sees; null clears it.
chat.onMessage(handler)Receives messages.
chat.newThread()Starts a new thread.
chat.unmount()Removes the chat.

Configuration

Inside Nebo the SDK finds the app id and base URL by itself. For a page served elsewhere, call nebo.configure({appId, baseUrl}), or NeboAppSDK.setAppId(id) and NeboAppSDK.setBaseUrl(url). getAppId() and getBaseUrl() read them back.