/docs / apps

Apps

An app is an employee with a page. Owners open it in its own window to see and work with something directly: a board, a tracker, a form. The page is plain HTML and JavaScript, and it talks to Nebo and to its employee through one script, the App SDK.

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"]
}
FieldMeaning
type"app". Makes the employee an app. Nebo also reads the older artifact_type.
idA UUID minted when the app is created. The page is served at /apps/<id>/ui/.
name, versionThe employee’s name and the package version.
windowtitle, width, height, resizable, fullscreen, orientation. Defaults: 1024 × 768, resizable, not full screen, portrait, titled after the employee.
permissionsWhat the page may do. Default ["storage:readwrite"].

Permissions

PermissionGrants
storage:readwriteThe 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:motionThe 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-*) and viewport-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-id and nebo-base-url meta tags.
  • Every file goes out with its real content type, including video, sound, fonts, wasm and 3D models (glb, gltf). A file with an unknown extension is sent as application/octet-stream and the browser will not use it.
  • Files answer range requests (206, or 416 for 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.html included, is checked on every open and answers 304 when 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_employee removes 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 zipWhat happens
AGENT.mdThe persona and the listing’s manifest.
agent.jsonThe employee’s configuration. {} is fine.
manifest.jsonKept 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 filesSkipped and uiFilesStored in 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-token instead, the page as a ui archive. See App Sidecar and Publishing.