The package
An app is an employee package with a manifest.json that marks it as an app and a ui/ folder with the page.
<data>/user/agents/deal-board/
├── manifest.json # marks the employee as an app
├── AGENT.md # the employee behind the page
├── agent.json # optional: sidecar tools, workflows
└── ui/
├── index.html
└── app.js {
"id": "<minted UUID>",
"name": "deal-board",
"version": "1.0.0",
"type": "app",
"window": { "title": "Deal Board", "width": 900, "height": 700, "resizable": true },
"permissions": ["storage:readwrite"]
} | Field | Meaning |
|---|---|
type | "app". Makes the employee an app. Nebo also reads the older artifact_type. |
id | A UUID minted when the app is created. The page is served at /apps/<id>/ui/. |
name, version | The employee’s name and the package version. |
window | title, width, height, resizable, fullscreen, orientation. Defaults: 1024 × 768, resizable, not full screen, portrait, titled after the employee. |
permissions | What the page may do. Default ["storage:readwrite"]. |
Permissions
| Permission | Grants |
|---|---|
storage:readwrite | The app’s key-value store. |
subagent:<employee-id> | Asking another employee from the page. The app’s own employee needs no permission. |
network:<host> or network:* | Requests to outside hosts through Nebo’s proxy. |
device:motion | The gyroscope and accelerometer for this page only. On iPhone the page also calls DeviceMotionEvent.requestPermission() from a tap. |
Every permission is prefix:scope. Ask for the least the app needs.
Full screen, orientation and tilt
Games and films can take the whole screen. Set fullscreen and, for the mobile app, an orientation: portrait (the default), landscape or any. Any other orientation is refused when the manifest is written.
{
"type": "app",
"permissions": ["storage:readwrite", "device:motion"],
"window": { "title": "Kart", "fullscreen": true, "orientation": "landscape" }
} - On desktop the app opens in a full-screen window.
- In the mobile app there is no app bar, the system bars are hidden, the screen stays awake and pull to refresh is off. A small Close pill sits in the top-left corner and fades after a few seconds; a touch near the top brings it back. The iOS edge swipe is off, and Android’s back button walks the page’s history before closing.
- Pad the page with
env(safe-area-inset-*)andviewport-fit=cover, and keep controls clear of the top-left corner.
The voice control
Every app opened in the Nebo desktop app or in the mobile app shows a small voice control in its bottom-right corner, so the owner can talk to the app’s employee while it works on the page. The control sits outside your page, so reloading the page never drops the call.
- On desktop it is a bar about 300 by 48 pixels, 16 pixels in from the window’s bottom-right corner.
- In the mobile app it is a round 48-point button inside the safe area that the owner can drag to either side. In a full-screen app it folds back to that button a few seconds into a call.
- Keep important controls clear of the bottom-right corner.
- During a long task on a call, the employee says short updates such as “Now editing.” and never reads out file names or commands.
Create an app
An employee can build an app two ways, and picks by what the owner says. “Build me an app for my orders” makes a new app employee. “You are the app” or “build yourself a game” turns the employee the owner is talking to into the app: it keeps its chat, memory and persona, and its page reloads while the owner talks to it. This works for an employee hired in conversation too. When it is not clear which the owner means, the employee asks once.
update_employee(
name: "<the employee's own name>",
app: { window: { title: "Solitaire", fullscreen: true, orientation: "landscape" } },
ui: { "index.html": "<!doctype html>..." }
) Everyday tools (a tracker, a form, a small dashboard) are built with Nebo’s built-in app skill. A page that should look designed rather than generated, such as a cinematic landing page, a scroll-driven film, generated art or a game, is built with the App Studio skill from the marketplace.
For a new app, the simplest way is to ask an employee. Nebo includes a built-in skill for building apps, and the
employee uses create_employee, which writes the folder, the manifest, the persona and
the page in one call. The app appears in the owner's workforce within seconds.
create_employee(
name: "deal-board",
description: "A board of open deals, sorted by close date.",
app: {
window: { title: "Deal Board", width: 900, height: 700, resizable: true },
permissions: ["storage:readwrite"]
},
ui: { "index.html": "<!doctype html>..." }
) update_employee takes the same ui, app and agent_md arguments. A path in ui replaces that one file. The page is read
from disk on every request, so reloading the window shows the change.
A minimal page
This page loads the SDK, asks who its employee is, and prints the answer. If it shows the employee's id and name, the wiring is right.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Deal Board</title>
</head>
<body>
<h1>Deal Board</h1>
<pre id="who">loading...</pre>
<script src="/sdk/nebo.global.js"></script>
<script>
NeboAppSDK.nebo.identity.get()
.then(me => { document.getElementById('who').textContent = JSON.stringify(me, null, 2); })
.catch(err => { document.getElementById('who').textContent = String(err); });
</script>
</body>
</html> The SDK finds the app id from the page's address. A page served from anywhere else needs <meta name="nebo-app-id" content="<id>">. Keep state in the app's storage,
not in the page: the window closes, and storage survives.
How the page is served
- Nebo serves
ui/at/apps/<id>/ui/and opens it in its own window. - A path with no matching file falls back to
index.html, so client-side routing works. - HTML pages get
nebo-app-idandnebo-base-urlmeta tags. - Every file goes out with its real content type, including video, sound, fonts,
wasmand 3D models (glb,gltf). A file with an unknown extension is sent asapplication/octet-streamand the browser will not use it. - Files answer range requests (
206, or416for a range past the end), so video and sound can seek. In the mobile app, video plays in place without a tap:<video muted playsinline autoplay loop poster="assets/hero.jpg" src="assets/hero.mp4"></video> - A file whose name carries a content hash, such as
main-0a8ksftt.js, is cached for a year. Everything else,index.htmlincluded, is checked on every open and answers304when unchanged. Build with hashed names rather than renaming files by hand. - Each page file may be at most 10 MB.
- Deleting the folder deactivates the employee;
delete_employeeremoves it entirely.
Add a sidecar
When the page needs server logic, its own data, or tools the employee can call, add a sidecar: a
binary Nebo runs next to the app. The page reaches it with neboFetch and a relative path. See App Sidecar.
Publish from Nebo
An app the owner made can publish itself. The owner says “publish yourself”, or taps Publish in the app’s chat, on the app’s screen in the mobile app (in the top bar, or as a pill beside Close for a full-screen app), or chooses Publish This App… from the desktop menu (the File menu on macOS while an app window is in front; the app window’s App menu on Windows and Linux). The employee drafts the listing from the app, takes 3 to 5 screenshots, and shows the listing in the chat to shape by talking. Nothing is sent until the owner answers Submit for review on the card, by tap or by voice.
An app the owner made always has its developer tools for itself: reload, status, console, screenshots and publishing. App Developer mode (Bot settings, Developer) lets other employees work on any of the owner’s apps, adds a floating console to the page, and turns caching off. Apps installed from the marketplace never get developer tools or a Publish button.
Publish an app yourself
A page-only app publishes as one bundle with the agent tool. The AGENT.md frontmatter must say artifact_type: app, or the marketplace creates
a plain employee with no page. Quote any value that contains a colon followed by a space; unquoted,
the frontmatter cannot be read and the app silently becomes a plain employee.
--- name: deal-board description: "A board of open deals: sorted by close date, with a chat that knows each deal." artifact_type: app metadata: version: "1.0.0" --- # Deal Board You work beside the Deal Board app. ...
agent(action: "create", name: "deal-board", version: "1.0.0",
description: "A board of open deals, sorted by close date.",
manifestContent: "<contents of AGENT.md>")
agent(action: "bundle-token", id: "<app id>")
zip -r deal-board.zip deal-board -x '*/.git/*' '*/node_modules/*' '*/src/*'
curl -X POST "https://neboai.com/api/v1/skills/<app id>/bundle" \
-H "Authorization: Bearer <token>" \
-F file=@deal-board.zip
agent(action: "submit", id: "<app id>", version: "1.0.0") | In the zip | What happens |
|---|---|
AGENT.md | The persona and the listing’s manifest. |
agent.json | The employee’s configuration. {} is fine. |
manifest.json | Kept as the app’s manifest: window and permissions reach the installed app. Must be valid JSON. |
ui/ | The page. Allowed: html, css, js, mjs, ts, jsx, tsx, json, map, txt, md, images (png, jpg, gif, svg, webp, avif, ico, bmp), fonts (woff, woff2, ttf, otf), sound (mp3, wav, ogg, m4a, aac, flac, opus), video (mp4, webm, mov, m4v), wasm, glb, gltf, pdf. Dot files and node_modules/ are dropped. |
skills/<name>/ | The employee’s own skills, checked like a skill uploaded on its own: SKILL.md kept, scripts/ and bin/ any type, everything else on the skill allowlist. |
- Limits: 10 MB per file and 50 MB per bundle. A file over 10 MB is skipped (check
filesSkippedanduiFilesStoredin the result); a bundle over 50 MB is refused. - The upload token lasts 5 minutes. A single top folder in the zip is stripped.
- For a private or loop app, every upload rebuilds the installable package at once and Nebo tells the bots that installed the app. A bot that is online puts the rebuilt package in place right away; one that is offline does it when it next connects. You don't need to raise the version for this, and the owner's settings, schedules and data for the app are kept. Public, unlisted and invite-only apps keep their approved package until a new version passes review.
- A new version follows each owner's automatic-updates setting for the app. It is off unless the owner turns it on, so by default the bot shows "Update available" and the owner applies it from Settings, then Updates, or from the Inbox. An update never grants new permissions; a permission the new version adds is asked for when the app first needs it.
- If NeboAI withdraws an app from the marketplace, every bot that installed it turns it off and tells the owner. Nothing is deleted: everything the app saved is kept.
- An app with a sidecar uploads each platform’s binary with
binary-tokeninstead, the page as auiarchive. See App Sidecar and Publishing.