Building apps, for agents

How an agent builds a Laze app people want to use: design rules, the kit, pins and sharing, trunk pushes and checking its own work.

This page is written for an AI agent asked to build or change a Laze app. Read it before your first laze app create. People judge an app in the first five seconds: make it look finished, put it where they asked for it, and check it yourself before you say it is ready.

The workflow

  1. Create it in the conversation that asked:

    laze app create "Insights" --channel <channel>

    The app starts empty, with a folder that is yours: build it from scratch, starting at index.html, then laze app push. --channel pins the app as a tab of that conversation. Everyone in it, and their agents, can open it and build on it (Builder). Name the app in words people read, not a slug.

    Link the platform kit and client from your page:

    <link rel="stylesheet" href="/_laze/theme.css" />
    <link rel="stylesheet" href="/_laze/kit.css" />
    <script src="/_laze/laze.js"></script>

    Slides, a deck or a talk are the one exception: they start from --template slides, the only template. Its files are the app's first live version, yours to change. Each slide is one file in slides/, listed in order in slides/deck.json; the template's README.md says how to add, reorder and style slides. The deck has a slide rail, keyboard navigation, full screen Present, and Export PDF and Export HTML buttons, so do not build those again. Over MCP, create_app with template: "slides" does the same, and write_app_files changes one slide at a time.

    The slides template is only for a new deck. When a person sends files that already are a page or a deck (an HTML export, a zip with index.html, a static site), put them up as they are: create the app with no --template, unpack the files into its folder with index.html at the top, copy in any script it loads from a CDN, and push. "Move them here" or "put this here" with an attachment means exactly that: do it, do not ask what it means.

  2. An app that already exists goes into a conversation with a pin, not a message with a link:

    laze app pin <app> --channel <channel>

    --role editor lets members change data but not the code, --view-only lets them only look. laze app pins <channel> lists the tabs. Never pin again an app a person unpinned.

  3. Shape the data in laze.app.json: one collection per kind of thing, with typed fields and a few seed records. Data lives in records, never in the page's code. A page that hard-codes its numbers, rows or text is wrong: when the data changes, the app must change without a push.

  4. Change the page, then look at it locally before it goes live (see Check your own work).

  5. Push:

    laze app push -m "Added the pipeline board" --seed

    Every push is the live app at once. There are no drafts, branches or pull requests: apps are trunk based. Write the message as a plain sentence about what changed for people. --seed fills only collections with no records yet.

  6. Go back when a push was wrong: laze app versions, then laze app revert <version> -m "<why>". That is a new live version; no record is lost. The git history inside an app is only for this.

Who can open it

VisibilityWho opens it
RestrictedPeople, agents and conversations it is shared with or pinned in, plus workspace Owners and Admins
ChannelMembers of the app's channel
InternalEveryone in the workspace
PublicAnyone with the address

A new app is Restricted. An app pinned in a conversation opens for everyone there, so a pin is usually all the sharing a team app needs. Do not share an app with only the person who asked when they asked for it in a channel: pin it there. Widen with laze app access visibility internal|public only when asked; the command says when an admin must approve. laze app members shows who can open it now. Check it before you tell people the app is ready.

Design rules

Start from what the app is about, not from a layout. Ask how someone who cares about the subject would draw it on a whiteboard, and build that: a threat model is an attack map, a roadmap is a timeline, a budget is a flow of money, a process is a diagram. A page of stat tiles and bullet lists reads as a report, and that is the app to avoid.

Be very visual: use motion graphics and flows where you can, and guide people one step at a time until they fully understand. Never write walls of text: where you need words, use simple, short sentences. An app is for people to understand, not for your own brain dump.

The Laze kit comes from the platform (/_laze/theme.css, /_laze/kit.css, /_laze/laze.js). It is a toolbox, not a layout to fill in: take its tokens and the controls you need, and put your own CSS in your own file on top. Never copy the kit into the app.

  • Visual first. Make the picture of the subject the app itself: inline SVG, canvas, a map, a timeline, a diagram, something people click and explore. Give it a look of its own, with your own layout and motion on the kit's tokens.
  • One job per screen. The first screen answers the question the app exists for. Put a second job on a tab (lz-tabs), not below the first.
  • The right shape. Use a table (lz-table) or a board (lz-board) when people scan or move rows, a stat tile (lz-stats, lz-stat-value) when one number is the point, a chart (lz-chart) for numbers over time and numbered steps (lz-steps) for a short sequence. Text longer than three lines belongs in a doc, not an app.
  • Real data. Show the records people care about, with their real names and dates. No "Lorem", "Item 1" or placeholder charts. Every claim on the page comes from a record or a link.
  • Type scale. One page title (lz-title), one line under it (lz-lede), section titles (lz-h2 or lz-card-title), body text. Do not add other sizes.
  • Spacing. Use the page (lz-page), card (lz-card lz-card-body) and grid (lz-grid, lz-row) classes; they keep the 12 to 20 px rhythm. Do not stack cards inside cards.
  • Both themes. Colours come only from tokens (var(--color-fg), var(--color-bg-surface), var(--color-primary), var(--color-border) and the lz-badge-* tones). A hex colour breaks dark mode.
  • Narrow width. Apps open in a side panel about 380 px wide. Give table cells data-label so rows stack, and keep headers to one line.
  • Empty and error states. An empty list says what will appear and how to add the first one. A failed call shows Laze.fail(error), never a blank page.
  • Status as badges. Show states with lz-badge and a tone (-success, -warning, -danger, -info, -primary), not coloured text.
  • Forms stay out of the way. Put "New item" behind a button; the list is the main content.

Kit reference

NeedClasses
Page and headerlz-page, lz-header, lz-title, lz-lede
Layoutlz-row, lz-grow, lz-grid, lz-stats
Cardslz-card, lz-card-body, lz-card-title, lz-h2
Tableslz-table-wrap, lz-table, lz-num (right aligned numbers)
Boardslz-board, lz-column, lz-column-head, lz-tile
Numberslz-stat-value, lz-chart, lz-chart-col, lz-chart-track, lz-chart-bar, lz-chart-value, lz-chart-label
Stepsol.lz-steps with one li per step
Controlslz-btn, lz-btn-primary, lz-btn-ghost, lz-input, lz-select, lz-textarea, lz-field, lz-form, lz-tabs, lz-tab
Stateslz-badge (+ tone), lz-callout, lz-empty, lz-muted

Scripts load only from the app's own origin: a CDN <script> is refused, so copy a library you need into the app folder. Porting a claude.ai artifact or another React page, compile its JSX before you push (for example npx esbuild app.jsx --bundle --minify --outfile=app.js) and link the built file. In-browser Babel works too, but it loads megabytes on every open.

In the page script, Laze.records("<collection>") lists, creates, updates and removes records, Laze.subscribe reloads on changes, Laze.h builds elements and Laze.mount draws them.

Keyboard shortcuts

Laze passes its own shortcuts from your app to Laze, unless your app handles them, so people can press Cmd+K (Ctrl+K on Windows and Linux) to leave an app without clicking out of it first. The shortcuts passed on are Cmd+K, Cmd+[ and Cmd+], Cmd+1 to Cmd+9, Cmd+Shift+A, Cmd+Shift+T, Cmd+Shift+M, Cmd+Shift+D, Cmd+. and Cmd+/, plus Alt+Up and Alt+Down outside text fields. Every other key stays in your app.

To keep one of these for your app, handle the key and call event.preventDefault(); Laze then leaves it alone:

document.addEventListener("keydown", (event) => {
  if ((event.metaKey || event.ctrlKey) && event.key === "k") {
    event.preventDefault();
    openSearch();
  }
});

Check your own work

Look at every change before you push it:

laze app serve --port 4173

It serves your folder at http://127.0.0.1:4173/ with the platform kit and the live app's records, read only. It needs a live version, so run laze app push once first. Open it with your browser tool and screenshot it in both themes and at panel width, for example:

npx -y playwright install chromium   # once
npx -y playwright screenshot --viewport-size=1100,800 http://127.0.0.1:4173/ light.png
npx -y playwright screenshot --viewport-size=1100,800 "http://127.0.0.1:4173/?theme=dark" dark.png
npx -y playwright screenshot --viewport-size=390,800 http://127.0.0.1:4173/ narrow.png

Look at each image. Fix text that overflows, columns that do not fit, colours that vanish in dark mode and anything that still says "demo".

Before you post the app:

  • It is pinned where it was asked for, and laze app members lists the people who should open it.
  • The first screen answers the request without scrolling.
  • Light, dark and 390 px screenshots look finished.
  • Your message gives the app's name and one line on what it does. The tab in the conversation is the link.

Ask when unsure

Ask the person one short question instead of building the wrong thing. Do not wait for a "go" on steps they already asked for: if they asked for an app in this channel, create it there, pin it and tell them.