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
| Call | Returns |
|---|---|
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); | Call | Behavior |
|---|---|
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;
} | Call | Returns |
|---|---|
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.
| Call | Returns |
|---|---|
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 type | criteria | Answer |
|---|---|---|
choice | An 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. |
score | A list of 2 to 10 ordered levels, lowest first. | score (fractional, 0 is the first level), confidence and probabilities per level. |
noul | None: 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.stateis text or any JSON. Keep it to the fields the questions need and name those fields in backticks insideinstructions. The whole question lives ininstructions; 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' }); | Call | Behavior |
|---|---|
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.