Skip to main content
After this guide, your web app has typed routes that read and write your agent’s data as the signed-in person, a page that calls them and follows the shell’s theme, and a release you can roll back. Start from Build and open your first web app if you have no app yet; About web apps explains the model. Verified against lua-cli 3.44.0. Before you begin
  • An app created with lua apps new <name>, with npm install run in its web/ folder.
  • @lua-ai-global/app-client installed in web/ (see Switch to the client package if your app has web/src/lua/client.ts).

Define the app

A web app is one file, src/apps/<name>/app.ts, that default-exports defineWebApp({...}). List it under webApps on your LuaAgent; lua apps new does both.
src/index.ts
defineWebApp takes: lua compile fails with a clear message when the name, a route key, or a nav entry is wrong, or when the app has no routes.

Write a route

A route key is a method (GET, POST, PUT, PATCH, or DELETE), one space, and a path. A path segment that starts with : is a parameter. Wrap each handler in route() so the schemas are checked and the handler’s arguments are typed.
src/apps/ops-dashboard/app.ts
route() takes: The handler’s return value is the response:
  • A value (an object, an array) answers 200 with it as JSON.
  • undefined answers 204 with no body.
  • json(value, status, headers?) sets the status. json(undefined, 204) answers with no body.
  • A Web Response is sent as it is.
A handler that throws answers with an error whose code is HANDLER_ERROR. Every error the platform produces has the body { code, message }; a body your handler returns is passed through as it is. A Zod .default() in a query or body schema fills the value in at runtime, but the handler’s type still allows undefined. Add ?? <default> where you read it, as query.status ?? 'open' above.

What a route can use

Routes run in the Lua sandbox, the same place as your tools and webhooks:
  • auth is the person using the page: userId, orgId, agentId, roles, and name and email when known. It is never an API key and never anonymous, so you can record who made a change.
  • Data and the other runtime objects work as they do in a tool.
  • env('KEY') reads the agent’s environment variables. Set them with lua env.
  • fetch calls your own APIs. The page never sees the keys you use here.

Call routes from the page

The page project in web/ is a normal Vite + React project. The template uses @lua-ai-global/ui components and Tailwind v4; add any npm package you need. The page talks to your routes through @lua-ai-global/app-client:
web/src/main.tsx
web/src/App.tsx
The client gives the page: The session lives in memory only. The page keeps no token in cookies or storage, and a reload asks the shell for a new session.

Run and test locally

lua apps dev <name> serves the page on the app’s own Vite server with hot reload, and runs each route in the sandbox on your machine as you. Data and env() are the live ones, so writes are real. lua test webapp calls a single route:
Locally auth.roles is empty: who may open an app is decided by the platform, not on your machine. See lua apps and lua test.

Release a version and roll back

lua push webapp builds the page with the app’s Vite and creates a web-app version. It is staged, not live. The agent version you create next pins it, and promoting that agent version makes it live:
Bare lua push and lua push all --force push web apps too. To roll back, promote the previous agent version: the app goes back to the version that agent version pinned. See Release an agent to production.

Who can open the app

Open a live app by URL. There is no menu entry for apps yet, and the native desktop app does not open them yet: The app opens full page. Anyone who can read the agent can open it: for an agent that is not private, every member of its organization; for a private agent, only the people who can see that agent. Anyone else, including people in other organizations, sees that the app does not exist or that they cannot open it.

Switch an app off

An admin can switch an app off, and on again, from the agent’s Apps page in the admin console. The change takes up to about 30 seconds to reach everyone. While it is off, the app does not open and its routes answer with the code APP_DISABLED.

Limits

lua push webapp refuses a build that breaks one of these before it uploads anything: Keep large data out of index.html, and rename source files whose built names would use other characters.

Switch to the client package

lua-cli 3.42 to 3.45 copy the page client into your app as web/src/lua/client.ts. To use the package instead:
Then change from './lua/client' to from '@lua-ai-global/app-client' in src/main.tsx and src/App.tsx, and push the app again.

If it isn’t working

The request did not match the route’s params, query, or body schema. LuaApiError.message says which field. Check the body you pass to lua.api() against the schema.
The app is not listed under webApps on your LuaAgent, or lua compile failed. Add the import and the webApps entry that lua apps new prints.
tsconfig.json must exclude src/apps/*/web. lua apps new adds it when your tsconfig.json is plain JSON; add it by hand when the file has comments.
A push is staged. Create an agent version and promote it.

Next steps

About web apps

Where apps open and how access works.

lua apps reference

lua apps new and lua apps dev.

Data reference

Every Data method your routes can call.

Release an agent

Agent versions, promote, and rollback.