Browse documentation
Docs/Functionality

Workbook Surfaces

Surfaces are workbook tabs that turn model data into a purpose-built interface. A sheet is the most familiar surface: it shows cells. Other surfaces show the same model as a table, map,…

Watch it in Grid

See the concept in Grid.

Use the film for the product interaction and this article for the complete contract and reference detail.

Workbook Surfaces

Surfaces are workbook tabs that turn model data into a purpose-built interface. A sheet is the most familiar surface: it shows cells. Other surfaces show the same model as a table, map, chart, board, document, app, or custom React view.

The Pro Research Workspace is deliberately different: it is one top-level, model-scoped workspace with five research workflows, not another persisted workbook surface tab. Experiments, Molecule, and Document surfaces can open it with bounded context, while the workspace retains one shared model research state. See Research Workspace.

Why Surfaces Exist

Models often need more than a grid. A sales pipeline may want an Airtable-like table and a Monday-style board. A location model may want a map. A forecast may want charts, controls, and a polished dashboard for people who should not have to inspect every formula.

Surfaces let those interfaces live inside the model instead of beside it. The spreadsheet remains the source of truth for formulas, inputs, and calculations; surfaces give that same state a richer user experience.

The UI Model

A workbook has a tab strip. Some tabs are sheets, and some tabs are surfaces. Both are backed by model state.

For a sheet, the tab name is a namespace and the visible items are cell addresses:

Sheet1!A1 = 100
Sheet1!B1 = A1 * 1.2

For a surface, the tab name is also a namespace, but the visible items are named slots:

Map_1!type = "map"
Map_1!config = """
version = 1
kind = "map"
title = "Stores"
"""
Map_1!data = ""

The important slots are:

Slot Purpose
!type Selects the surface family, such as table, map, or component.
!config Stores the surface settings: bindings, layout, field names, titles, and options.
!data Stores surface-owned interactive state, such as board cards, app-builder trees, diagram positions, or layout tiles.
!source Stores editable JSX source for custom or ejected surfaces.

The UI reads those slots and chooses the matching renderer. A surface can be native, custom, or ejected:

Mode Meaning
Native A built-in Grid interface renders the surface from !config and !data.
Authored A Component surface renders author-written JSX from !source.
Ejected A built-in surface generates JSX, stores it in !source, and then renders through the custom UI path.

This is the foundation for Grid's application layer. Built-in surfaces provide familiar app-like views over model data. Custom surfaces use the same tab, binding, and persistence model, so a workbook can grow from spreadsheet to application without moving state into a separate front-end project.

Grid Apps

Grid Apps are the application layer that sits on top of the reactive spreadsheet model. The model remains the state/program; App Mode is the end-user runtime; Dev Mode is the workbench for inspecting and changing the model behind it.

Surfaces can run in two product contexts:

Mode Meaning
Dev Mode The normal Grid workbench: sheets, source editing, inspectors, surface configuration, and model debugging.
App Mode A takeover runtime shell for people using the model as an application.

App Mode is not a separate model format. The spreadsheet model remains the reactive state engine, while the active Surface or packaged model UI is the presentation layer. A promoted Surface can become an app entry point, navigate to sibling surfaces, and push browser history so Back/Forward behaves like app navigation. A model can also carry a portable ui/ package inside its .gridoc; when present, Grid can render that package as the model-owned app entry instead of the workbench. Packaged apps may declare a homePath in ui/manifest.json so the model's default App Mode launch opens a logical app route such as dist/dashboard while the physical HTML entry document remains dist/index.html. Explicit deep links still use ?gridMode=app&appPath=<path> and win over the declared home route. They may also declare an app block in the same manifest: app name, short name, description, package-relative icon, safe theme hints, and shell chrome. That is the piece that lets a model opened from the Models page behave like a custom application. Generated App Builder packages publish frameless app chrome by default, with a small floating Dev Mode affordance and the keyboard escape chord preserving the path back to the workbench. Generated App Builder packages also write dist/app-meta.json, a package-local metadata sidecar that describes the app as an app: pages, home route, declared model reads and writes, requested connector capabilities, component families, layout primitives, conditions, commands, row-scope usage, and higher-level features such as navigation, responsive layout, record views, filters, charts, feedback, model writes, and the app-framework readiness state. It also records detected app patterns such as routed app, dashboard, records workflow, input workflow, or static screen. The sidecar is not the launch manifest; it is the inspection contract that lets Builder tools, custom UI tooling, audits, and future app catalogs understand the UI framework sitting on top of the reactive model without parsing generated JavaScript. They also include dist/GRID_APP.md, a short package-local handoff guide for authors replacing or extending the generated UI. It lists the app home, pages, read/write model contract, features, connector module, and state helper in the same ui/ package that Grid serves. Its starter snippet is generated from the app's declared reads and writes, so custom UI starts from the actual model contract rather than a blank template. Generated packages also include a custom-ui/ handoff project. Its custom-ui/manifest.json declares the custom project contract, custom-ui/README.md explains the replacement flow, custom-ui/index.html is the replacement entry path, custom-ui/main.jsx is a React entry over the generated React adapter, custom-ui/vue-main.js is a Vue entry over the generated Vue adapter, and custom-ui/grid-app.d.ts re-exports generated app-specific symbol/type helpers. The default app still launches from dist/index.html; the custom project is the explicit handoff folder for replacing or building over that generated entry. Generated packages also include dist/custom-app-starter.js, a runnable plain-JavaScript starter over dist/grid-app.js and the app's declared symbols. It is not the default entry; it is a concrete replacement starting point for authors who want to take over app.js. They also include the stable framework interface modules dist/grid-app.js, dist/grid-react.js, and dist/grid-vue.js, with matching declaration files. grid-app.js exports the generated app contract, symbol constants, write guards, and action helpers such as setAppValue, setAppBatch, appendAppRow, routeApp, runAppJob, and callAppConnector. grid-react.js exports a React useGridApp hook over that contract; grid-vue.js exports the Vue useGridApp composable. The dist/custom-react-starter.jsx and dist/custom-vue-composable.js files are now examples that consume those adapters rather than being the interface custom code has to copy. Generated packages also emit dist/grid-connector.js, an ES module exporting the same grid connector object used by the generated app renderer, plus dist/grid-connector.d.ts with the connector's TypeScript shape. Custom React, Vue, or hand-written UI inside ui/ can import that module instead of re-implementing the grid:model-ui:v1 postMessage protocol, so generated and custom apps sit on the same typed reactive model connector. The module also exports createGridStore(symbols), a framework-neutral reactive store over grid.subscribe. React can adapt it through useSyncExternalStore, Vue can subscribe to it from a composable, and plain JavaScript can listen directly, while all state still lives in the spreadsheet model. For app code that wants fewer moving parts, createGridState(symbols) wraps that store with value, rows, set, setBatch, appendRow, setFormula, location, onLocation, route, runJob, callConnector, dev-mode escape, and lifecycle helpers. The generated App renderer uses that same app-state helper internally, so the visual Builder path and fully custom UI path share the same reactive state adapter, including optimistic local updates for set, setBatch, and appendRow. Generated and starter apps also handle Shift/Cmd/Ctrl+Escape inside the app frame by calling model.enterDevMode(), so takeover UI retains a predictable return path even when keyboard focus is inside custom UI.

When a model has both a packaged ui/ app and a promoted Surface entry, the packaged app is the default model-owned app entry. Explicit Surface URLs still win: ?gridMode=app&surface=Table_1 opens that Surface takeover even when the model also has ui/. If an explicit Surface target is missing, Grid falls back to Dev Mode rather than silently opening the packaged app.

Dev Mode is always recoverable. App shells expose a Dev mode control, explicit URLs can force ?gridMode=dev, and the escape chord Shift+Esc / Cmd+Esc / Ctrl+Esc returns the user to the workbench. For packaged custom UI, Grid injects the iframe-side escape listener when serving HTML so the shortcut still works even if focus is inside arbitrary app code.

Built-In Surfaces

Built-in surfaces are ready-made interfaces for common model workflows. They are designed for direct manipulation: filter a table, edit a board card, inspect a map layer, shape a dataset, or arrange a dashboard without leaving the workbook.

Current built-in surface families include:

Surface What It Is For
Predict Training, registering, and calling a model-backed prediction function.
Dataset Importing, previewing, shaping, and preparing tabular data.
Table Airtable-style browsing and editing over row data.
Map Spatial layers, location data, and geography-heavy models.
Calendar Event and schedule views over date/time rows.
Charts Dense analytical charts for model outputs.
Visuals Presentation-oriented SVG charts and dashboards.
Candlestick Financial OHLC charts with overlays and realtime preview.
Document Narrative, reports, notes, and embedded model context.
Notebook Reactive computational narratives with Grid code, live values, inputs, CLI tools, and provenance.
Board Freeform planning boards with cards, notes, and embeds.
Layout Dense dashboard tiles for values, formulas, and notes.
Kanban Workflow boards backed by rows or surface-owned cards.
Diagram Node-link diagrams for networks, flows, and relationships.
Sketch Parametric 2-D engineering geometry, constraints, dimensions, and CAD export.
Analysis Finite-element fields, convergence, mesh previews, and saved probes.
Requirements ReqIF requirements, traceability, verification records, baselines, and change impact.
Engineering Inspection of normalized IFC, FMU, Gerber, and Excellon artifacts.
Industrial Asset, telemetry, alarm, work-order, and maintenance supervision.
Sequence Sequence, read, alignment, and sequence-index artifact workflows.
Variant Region-based variant inspection and explicit variant-calling workflows.
Molecule Chemical graphs, structures, fingerprints, docking, and trajectory workflows.
Canvas Computed drawings or numerical pixel images bound to a model value.
Experiments Simulation matrices, run comparison, trajectories, calibration, and evidence.
Discussion Comment, forum, or chat-style views over discussion rows.

These are the surfaces to reach for when the model fits a familiar product shape: tables, boards, dashboards, documents, maps, calendars, and charts.

Predict

The Predict surface owns the lifecycle of a model-backed prediction workflow. It lets a user choose a learner, pick feature data from a range or file, train or register a model artifact, and expose a callable prediction function back to the workbook.

Use it when a spreadsheet model needs a prediction step but the user should not manage training files and function wiring by hand. The surface keeps the model name, learner family, feature source, target column, deployment status, exposed function name, and last metrics together in one tab.

XGBoost regression additionally offers Calibrate an automatic upper P90 bound. This selects a seeded held-out calibration, not a full predictive distribution. Automatic mode may still produce a point-only model with an unavailability reason. The deployment view exposes a bound formula only for an admitted exact registry version; bind a worksheet feature range to copy it. Follow Train and Use Calibrated Predictions for data requirements, candidate-versus-pinned versions, external artifacts, and failure handling. Training requests also support required calibration, temporal splits, and explicit probability levels.

Dataset

The Dataset surface is for bringing tabular data into model shape. It can preview data from model values, files, or configured sources; show schema information; and apply a pipeline of transforms such as filter, select, rename, derive, sort, limit, dedupe, and aggregate.

Use it when raw data needs cleaning before it feeds formulas, tables, charts, or custom apps. Dataset is the surface that turns "we have a CSV or row array" into "we have a model-ready source."

Table

The Table surface is the Airtable-like record view. It presents rows and fields with density, column, schema, and browsing controls that feel natural for record work rather than cell work.

Use it for operational data: customers, orders, tasks, inventory, leads, assets, or any other row set where users think in records. A Table can bind to a named row source or model-local table handle, infer fields when needed, and provide a familiar tabular interface over the live model.

Map

The Map surface renders spatial data as layers. A layer can bind to rows with latitude/longitude fields or geometry fields, choose visual style, and display labels or identifiers.

Use it for territory plans, store locations, delivery zones, facilities, incidents, assets, or any model where geography is part of the answer. The Map surface gives spatial model state a visual inspection layer rather than forcing users to read coordinate columns.

Calendar

The Calendar surface turns date/time rows into month, week, day, or agenda views. It maps event fields such as start, end, title, and color.

Use it for schedules, launches, staffing, bookings, deadlines, content plans, and project timelines. Calendar is best when the model output is naturally understood as time blocks rather than rows.

When bound to named date rows, the Calendar is editable in place: drag an event to reschedule it, drag its edge to resize it in the week and day time grids (with half-hour snapping), quick-create by clicking an empty slot, or click an event to edit its mapped fields or delete its row after confirmation. Each edit writes back to the bound cells, so the model stays the source of truth.

Charts

The Charts surface is for dense analytical charting over model outputs. It supports familiar series types such as line, bar, area, pie, and scatter, with field mappings for x, y, label, and color.

Use it for exploratory analysis, reports, and dashboards where the important question is "what changed, how much, and compared to what?" Charts is tuned for data-heavy analytical views.

Visuals

The Visuals surface is the presentation-oriented sibling of Charts. It focuses on polished SVG visuals, palettes, legends, grid options, grouped bars, curves, donuts, and hover behavior.

Use it when the model needs a chart that feels ready to show: executive dashboards, narrative reports, embedded presentations, and cleaner visual summary tabs. Finished visuals can be exported as SVG or PNG for use outside the workbook.

Candlestick

The Candlestick surface renders OHLC financial data with optional volume, overlays, indicators, and a live demo feed when no data source is bound.

Use it for market data, trading models, price simulations, and time-series analysis where open, high, low, close, and volume matter. It keeps chart settings, bound fields, realtime behavior, and overlays in one surface.

Document

The Document surface is for narrative around model state. It can show markdown or rich document content, bind to a live string body, and carry report-style themes.

Use it for memos, model explanations, operating notes, investment writeups, policy summaries, launch docs, and reports that need to sit next to the live calculation. Document makes the workbook readable to people who need context before cells.

Notebook

The Notebook surface is a computational narrative over the resident Grid model. Its ordered blocks mix markdown, Grid formula assignments, live values and ranges, scenario inputs, WHY provenance traces, and commands from the interactive Grid CLI. Formula blocks do not start a second notebook kernel: submitting one edits the model source through the normal formula-write path, and its output then participates in the same reactive calculation graph as the rest of the workbook.

Use it for exploratory modeling, scenario labs, reproducible investigations, model validation, and handoffs where the reasoning matters alongside the answer. Live value and range blocks always resolve from model state. Command and provenance output can be persisted with the notebook so a printed or shared copy retains the evidence that produced the conclusion. Each execution stores a bounded receipt with its principal, status, duration, model revision, timestamp, and truncation state. Captured output is marked stale after the resident model advances. The backend reparses every request and enforces an effect allowlist: model-changing, administrative, raw-RPC, source-replacement, temporary-model evaluation, and streaming CLI commands are blocked inside notebook command blocks. Writes use typed input and formula blocks instead.

Formula writes are acknowledged and compare-and-swap guarded by the model source hash. A formula block durably owns its output symbol; another block or an existing source definition cannot be overwritten without an explicit Take over symbol action. Formula execution uses a dedicated Notebook route: the server derives the Notebook, block, and authenticated principal identities from the route and session, then injects ownership into the internal model-runtime request. The generic batch editor rejects client-supplied Notebook ownership metadata. Each formula attempt carries a persisted retry identity and returns the same principal-bound execution receipt when an ambiguous network retry reaches the same backend. Reusing that identity with different content is rejected as a conflict. Notebook document saves use the data symbol's request revision, serialize through a single queue, and preserve local edits when a remote revision wins. Production WHY runs are refused unless the runtime temporal ledger is enabled on durable storage.

Notebook is a dedicated surface rather than a mode inside Document because it owns execution and write semantics. It still follows the same surface lifecycle: native blocks live in Notebook_1!data, and Eject to code creates editable JSX. In the ejected snapshot, model reads and input controls remain live while native CLI and WHY results are labelled snapshots.

Board

The Board surface is a freeform canvas for planning. It can hold text blocks, cards, embeds, and positioned items stored in the surface's data slot. It can also project bound card rows as read-only cards.

Use it for brainstorming, planning, model walkthroughs, lightweight dashboards, and mixed visual work where a grid is too rigid. Board is spatial and flexible: place the pieces where they help the story.

Layout

The Layout surface is a dense dashboard grid. It arranges value tiles and text tiles into a fixed set of rows and columns, with each block bound to a literal, a formula reference, or a note.

Use it for operating dashboards, KPI panels, trading-terminal-style views, scenario summaries, and compact executive screens. Layout is more structured than Board and more dashboard-like than Sheet.

Kanban

The Kanban surface is a workflow board. It can own cards directly in !data, or project cards from bound rows grouped by a status field. Columns carry titles, colors, and optional work-in-progress limits.

Use it for tasks, sales stages, recruiting pipelines, editorial workflows, incident response, model review queues, and any process that moves through states.

When cards are projected from bound rows, moving a card between columns writes the new status back to the model. Cards can be dragged with the pointer or moved by keyboard for accessibility, and bound cards can be edited through a modal. As with every surface, the bound cells remain the durable record.

Diagram

The Diagram surface is a node-link canvas. It can own editable nodes and edges or project them from bound node and edge row sources.

Use it for dependency maps, system diagrams, org relationships, process flows, network analysis, scenario graphs, and any model where relationships are easier to see as connected nodes than as rows.

Sketch

The Sketch surface is a parametric two-dimensional engineering canvas. It owns stable line, circle, and arc entities plus their geometric and dimensional constraints in !data. Dimensions can be driven by live model symbols, so a sheet edit regenerates the geometry; editing a bound input dimension in the inspector writes through the normal model input path.

Use it for profiles, plates, layouts, mechanisms, tolerance studies, design tables, manufacturing outlines, and spatial engineering models where geometry must stay connected to calculations. The native inspector reports degrees of freedom, constraint residuals, bounds, path length, and areas, and exports SVG or DXF for downstream CAD and fabrication tools. Named design configurations overlay unbound dimensions and publish a comparison table of measured outputs; model-bound dimensions keep external ownership and cannot be shadowed by a configuration.

When the active solid result supplies mass properties, enter a target such as MassSnapshot under Publish mass properties to cell, then choose Publish properties. Query the saved result with ordinary formulas:

solid_volume = CAD_VOLUME(MassSnapshot)
solid_area = CAD_SURFACE_AREA(MassSnapshot)
solid_center = CAD_CENTER_OF_MASS(MassSnapshot)

This publishes a persisted, revision-pinned cad_mass_properties_snapshot; it does not keep a process-local solid handle or automatically republish on later Sketch edits. After changing the design, inspect the newly computed properties and publish again deliberately. CAD_MASS_PROPERTIES validates a snapshot, and the query functions accept an optional expected source-revision string when the consuming calculation must require an exact revision. Volume and area carry dimensions; center of mass is a dimensional three-coordinate vector. Unavailable or uncertifiable area/center results return #N/A, not a fabricated zero.

Analysis

Use Analysis to inspect finite-element regions already authored in model source. Add the surface, choose Resolve regions, then select a region, field, and component. Inspect convergence/status and units before interpreting the color plot; the field table and mesh are bounded previews, not proof that every entity is visible.

Add a probe by entity index, or select a displayed node for a node field. Region, field/component selection, and bounded probes are saved with the tab. Change mesh, materials, loads, and boundary conditions in model source, then resolve again. The surface does not introduce a separate finite-element solver or make an unconverged result converged by displaying it.

Requirements

Open a saved model, add Requirements, and Import ReqIF from a bounded ReqIF 1.2/1.2.1 document or a single-document ReqIFZ archive. Use Requirements, Traceability, and Verification views to inspect paged objects, typed attributes, hierarchy, links, and conflicting evidence. Select a requirement to record verification with its method, observation time, and evidence references when write access permits. Export ReqIF exports the normalized artifact, not arbitrary embedded archive content.

Under Baselines & change impact, name and publish a baseline of the current artifact. After importing or creating the changed state, enter the exact before and after artifact IDs and choose Publish change impact. Inspect changed identifiers and suspect links; retain the resulting artifact references for review. Baselines and structural comparisons do not confer approval, signoff, promotion, or release status. Those are separate authorized workflows.

The formal Requirements signoff workflow, when installed and configured, requires the exact baseline and complete matching impact report, reviewer submission, then a different approver's reason and trusted signature. An incomplete/truncated comparison cannot be used as complete signoff evidence. The resulting workflow record remains tied to those artifacts; subsequent changes do not inherit approval.

Engineering

Use Engineering to inspect imported engineering files without executing them. Choose the IFC, FMU, or PCB view, select the corresponding file, and import or inspect it. The tab retains the artifact reference and selected view, so reopening it reads the same normalized artifact rather than reparsing an untracked browser file.

IFC inspection exposes normalized building entities; FMU inspection exposes model metadata and variables without loading its executable code; PCB inspection handles the supported Gerber/Excellon input. Browser import ceilings are 256 MiB for IFC, 64 MiB for FMU, and 32 MiB for PCB, with bounded previews. This surface is not an IFC geometry editor, FMU simulation runner, electrical design-rule checker, router, CAM tool, or fabrication/machine controller.

Industrial

Bind the surface's assets, telemetry, alarms, work orders, and maintenance collections to normalized model rows or tables. Start with Overview, select an asset or alarm for detail, and use Refresh endpoint status to inspect declared connector health. Missing or unavailable status is not evidence that equipment is healthy.

Supervisory actions require a configured action and an explicitly authorized host bridge. The default workbook host does not grant those action bridges. When one is available, select its target, click the action, review its confirmation, then choose Confirm manual action. Refresh, telemetry changes, and timers never trigger it. Adding this surface does not install a control loop or grant industrial write authority.

Sequence

Use Sequence for resident references, reads, alignments, and related artifacts. Import browser text formats FASTA, FASTQ, SAM, or Matrix Market, or select a supported file already in the model's Files area. Browser imports are bounded to 60 MiB. Binary BAM and H5AD use the linked-asset path, not the text upload path; backend support is still required.

Select the imported artifact, inspect its identity/summary, then choose an operation available for that artifact kind. Examples include a sequence sketch, mapping index, reading-frame translation, open-reading-frame search, and alignment. Mapping reads requires the reference and its index as explicit dependencies. Follow the job status and select the resulting artifact; importing a file alone does not run every analysis.

Variant

Use Variant for a bounded genomic region and variant artifacts. Import VCF text (up to 60 MiB through the browser), or use supported linked VCF/BCF data. Select the exact artifact and set the contig and start/end coordinates for the region view. A bounded result page does not represent the entire cohort.

Call variants from alignments is an explicit operation: supply the reference-sequence and read-alignment artifacts, run it, and inspect the resulting artifact and job outcome. A VCF import does not infer missing reference provenance, run a caller automatically, or establish clinical interpretation.

Molecule

Use Molecule to inspect chemical graphs, structures, and trajectories. Import SMILES, SDF, MOL2, PDB, or mmCIF text, or select a supported model asset; browser text imports are bounded to 60 MiB. Select the artifact and, where applicable, the structure model or trajectory frame.

Operations depend on artifact kind: molecule sets offer fingerprints, conformers, and ligand docking; structures offer dynamics preparation or receptor docking; fingerprint sets offer comparison/screening; prepared dynamics systems can run dynamics; trajectories can be analyzed. Choose the required receptor, ligand, or comparison artifact explicitly and follow the job result. Availability depends on the installed scientific capability; a viewer or artifact row alone does not establish backend availability or scientific validity. Research Workspace is a separate context-aware workspace, not an automatic side effect of these operations.

Canvas

Use Canvas to display a computed drawing or image, rather than manually edit a Sketch. Set source.bind to a cell holding a DRAW_* drawing, a supported DRAW_TO_SVG result, or numerical pixels produced by DRAW_RENDER/IMAGE_*. The view refreshes when the bound value changes.

For numerical pixels, pixels.mode = "gray" reads grayscale intensities and "rgb" reads packed RGB colors. layout.fit chooses "contain" or "actual"; show_checkerboard controls the background. Nothing to render means the binding is missing or its value is not a supported drawing/pixel shape. Correct the producing formula or binding instead of pasting arbitrary HTML into the canvas.

Experiments

Use Experiments to compare simulation candidates without editing the live model's inputs for every trial. In Design, name the experiment, select Time simulation, Discrete event, or Spatial agents, choose outputs, and supply the relevant step/integrator or region settings. Time-simulation sweeps use a full-factorial product of explicit values or linear ranges and scenarios; other design-of-experiments algorithms are not implied. Review the expanded matrix size before running it.

In Runs & comparison, follow the queue, inspect failures and returned summaries, compare runs without rerunning them, and Inspect trajectory for verified replay of a completed, successful time-simulation row. Active/failed rows, discrete-event/spatial-agent rows, and older records without complete replay evidence do not support this view. Publish evidence creates a pinned results artifact; it does not approve or release a model. Graduate creates a model branch from a selected run using the explicit target model ID, not a software release or silent replacement of the current model.

In Calibration, select a loss symbol, bounded parameters, tolerance, and maximum iterations. Run calibration returns the final optimum and counts. Run & publish evidence deliberately reruns the optimization and retains bounded, hash-complete evaluation evidence; it does not merely attach evidence to the prior result. The current synchronous calibration operation exposes no iteration progress, cancellation, or residual vectors. This simulation-fitting workflow is distinct from Predict's calibrated upper-P90 model bounds. See simulation authoring for model and replay semantics.

Discussion

The Discussion surface renders chat or forum-style conversation from bound rows or local preview messages. It maps author, body, timestamp, thread, and reply fields when present.

Use it for model review, operational notes, comment streams, decision logs, and collaborative context. Discussion keeps conversation close to the model state it is about.

Custom UI Surfaces

Custom surfaces are for model-specific applications. They let a workbook carry its own UI layer: forms, dashboards, workflow tools, calculators, decision screens, or embedded mini-apps that read from and write to the model.

There are two custom UI paths:

Surface What It Gives You
App A structured UI builder for composing model-bound screens from reusable blocks.
Component An authored React/JSX surface for custom interfaces written directly in the model.

App surfaces are the lower-code path. They are useful when you want to assemble a screen from standard UI blocks, bind those blocks to model values, and keep the interface editable by model authors.

Component surfaces are the code-first path. They are useful when the surface needs custom layout, custom interaction, or React ecosystem components. A Component can read live model values, render them through React, and write back to allowed model symbols.

Custom UI availability is a deployment trust decision expressed as one setting: VITE_GRID_CUSTOM_UI_TRUST = off | trusted | sandboxed-only. off disables every custom-UI runtime (including packaged app takeover); trusted enables them all — the single-author / trusted-team posture; sandboxed-only disables in-process JSX evaluation (Component surfaces and ejected built-ins) while keeping the App builder's no-eval interpreter and sandboxed packaged apps — the multi-tenant posture. When the policy is unset, the legacy VITE_GRID_COMPONENT_SURFACE / VITE_GRID_APP_SURFACE flags keep their existing behavior. Public embeds never evaluate in-process JSX regardless of the policy. Disabled runtimes keep their surface data; the tabs render an explanatory notice instead.

Packaged desktop builds default this policy to sandboxed-only. That makes the no-eval App builder and Publish Grid App available in the desktop product without trusting authored Component JSX. A desktop product build may still set an explicit supported policy when its deployment boundary calls for off or trusted; invalid values fail the build instead of silently disabling the builder through the legacy fallback.

One model-binding ABI

Every custom-UI runtime speaks the same verb set — the grid:model-ui:v1 model-binding ABI: read verbs (model.symbols, model.range, model.subscribe), write verbs (model.setInput, model.setBatchInput, model.setFormula), route verbs (app.location, app.setPath), and command verbs (job.run, connector.call). There are two transports: packaged apps speak it over postMessage through the host shell, and Component surfaces bind it in-process over the live symbol store. The generated package client derives its method table from the shared ABI module at publish time, and a conformance suite drives the packaged client and the reference client through identical wire sequences, so the runtimes cannot drift apart.

Capabilities are one schema everywhere: verb scopes (model.read, model.write, app.route, job.run, connector.call) plus per-symbol bindings.reads / bindings.writes. A Component surface declares them in its !config; a published package carries them in ui/manifest.json; the App builder's capability review renders them before publish. Writes are enforced at every execution point with the same guard: the Component runtime blocks undeclared writes in-process, and the packaged-app host shell rejects writes outside the manifest allowlist (write_denied) even when the coarse model.write capability was granted. The command verbs are capability-gated end to end; hosts that expose no job runner or connector bridge answer method_unavailable rather than pretending.

App

An App surface is a structured builder for custom model UI. It stores a tree of UI blocks in !data, mirrors declared reads and writes into !config, and can run in a preview mode against live model bindings. The App document is now an app-level structure, not only one component tree: it can contain pages, each with a title, package-relative path, and root component tree. The active page is mirrored into the legacy root field so older single-page App surfaces still render and publish.

Use App when the user wants a custom screen but still benefits from a visual composition model: metrics, inputs, cards, rows, lists, buttons, and repeated sections. The builder is direct-manipulation: drag blocks into place, multi-select and move or resize them, drop across pages, and grab and position blocks from the keyboard. App is also a stepping stone to code; when the user needs full custom behavior, an App can be ejected into a Component. The layout system is now part of that app model rather than incidental CSS. Alongside Stack, Row, Card, and Form, builders can use Section, Responsive grid, and Sidebar layout primitives. Sections provide page-level regions with title, subtitle, spacing, padding, and surface treatment; grids provide responsive dashboard/form/card regions with explicit or auto-fit columns; sidebar layouts provide a two-region workspace that collapses for narrower app frames. Layout presets in the palette seed dashboard grids, form sections, and record workspaces, and the same nodes render in builder preview, ejected JSX, and packaged ui/ apps. When published as a model-owned UI package, App buttons can also act as route buttons: action = "route" treats the button value as a package-relative appPath and updates the takeover URL without declaring a model write. The builder exposes the App document's pages as route targets so authors can wire navigation by choosing a page path instead of memorizing package URLs. Route buttons can optionally write a selected-state value before navigating. Inside a Repeater, that write can come from row scope (@order.id, @order.#index), which gives App Builder the standard master-detail app pattern: choose a record, commit that choice to the model, then open a detail page that reacts to the same state cell. Submit buttons are the batch-command sibling of route buttons. A submit action can write several model symbols at once, can take values from the current Repeater row (Target = @order.amount), and can optionally route after the writes. That gives visual App Builder screens a reusable command layer for review, approve, apply, save, and wizard-style workflows without moving state out of the spreadsheet model. Alerts are the feedback primitive for that same workflow layer. An Alert can bind its title or message to a model cell and use normal visibleWhen conditions, so validation errors, save results, approval status, or warnings can be calculated in the spreadsheet layer and shown in the app shell. The same condition system gates actions: disabledWhen can disable inputs, buttons, submits, or whole containers from model state or row scope, and that behavior is preserved in preview, ejected code, and packaged model UI. Published App packages declare a homePath from the selected home page, so opening the model in App Mode starts at a stable app route rather than the generated dist/index.html document. The generated package listens to Grid's app.location connector events and renders the page whose path matches the outer app route, while all reads/writes still flow through the model binding contract. The builder preview uses the same page-path rules, so route buttons can be tested before publishing. Ejecting an App to a Component also preserves the page table with a small generated router; the ejected code can navigate locally and still call the host model.setPath API when available.

This makes App Builder the visual framework layer for Grid Apps: pages define navigation, layout primitives define responsive regions, bindings define reactive state edges, actions define writes or route changes, and publishing turns the result into the model-owned ui/ package. Component surfaces remain the escape hatch for custom code, but they sit on the same connector and app-mode runtime. The Builder left rail includes an App framework panel generated from that same contract, so authors can see the app home, pages, model reads and writes, commands, row scope, conditions, feedback, and record/chart/filter features while they build instead of discovering those relationships only after publish. That panel now treats App Builder as a small application framework map: App shell, Reactive state, Actions, Data views, and Custom UI each carry a status, signal, and recommended action from the same metadata written to the package. Empty or partial layers can launch the existing shell/form/records starters directly, keeping the authoring conversation centered on the app being assembled rather than only on the URL that will launch it. The same metadata now rolls those layers into an App readiness strip (draft, forming, usable, or publishable) with a ready-layer count and a single next action. That gives builders a product-level path through app composition instead of asking them to infer intent from component counts alone. It also records a layout summary: section count, responsive grid count, sidebar layout count, card/form count, responsive-region count, and max explicit grid columns. Generated GRID_APP.md includes the same summary so a custom React, Vue, or plain JavaScript handoff can see whether it is replacing a dashboard grid, a form flow, a record workspace, or a basic stack. The framework panel also includes a capability review before publish. It summarizes the app's current risk level and lists every scope the generated app does or does not use: model reads, model writes, app route changes, job runs, and connector calls. Read-only apps show as low risk, model writes show as medium risk, and write-then-route workflows are highlighted as higher risk. Inactive job and connector scopes are shown explicitly so builders can see that a packaged app is not silently running jobs or calling external connectors. The palette now starts with inferred App patterns from the live model: Model app, Dashboard, Records workflow, and Input workflow. These apply the same model-aware starter logic as the framework map, using row arrays, selection/search state, scalar inputs, and feedback cells to assemble higher-level app flows instead of isolated widgets. The starter palette also exposes named app templates: CRM, Approval workflow, Forecast dashboard, Intake form, Operational queue, and Scenario planner. Each template binds live row sources, selected/search state, scalar assumptions, status/approval cells, and feedback cells when the model exposes them; when a symbol is missing, the template keeps the region as an unbound App component so the builder can wire it later instead of failing generation. The palette also exposes layout presets that do not require a model symbol: Dashboard grid, Form sections, and Record workspace. They are reusable container patterns rather than generated launch plumbing, so authors can first shape the app screen and then bind model state into it. Actions are now described as first-class action plans in the Builder contract, not just as incidental button props. Button plans can set, increment, decrement, write formulas, append a row by replacing array/table state, validate against a model condition, branch to a success route, write feedback, or declare planned job/connector calls for publish review. Submit buttons are batch transactions: their write lines run together, can resolve row-scope values, can route after success, and can write feedback. Generated dist/app-meta.json records an actions summary plus per-action plans with kind, reads, writes, validation, route-after-success, feedback target, capability scopes, support status, risk, and evidence. The framework panel and GRID_APP.md render the same Action Builder section so custom UI handoff can preserve action intent instead of rediscovering behavior from generated code. The adjacent Appearance panel persists app-level mode, accent, density, and radius on the App document. Builder preview, read-only run mode, generated ui/ packages, and ejected Component JSX all consume that same theme contract, so app styling is a first-class framework layer instead of loose CSS drift. The App state panel is now a semantic binding browser over the current reactive model contract. For every declared read/write symbol it shows access (Read, Write, or Read/write), binding role (table schema, writable app state, action target, or derived value), live kind/preview, source lineage (Named state or sheet/table source), row schema, a small sample row when the model exposes one, validation hints, and suggested UI uses such as Table, Cards, Chart, Number input, Toggle, Alert, or Metric. Rows can jump back to the source model symbol through the existing selection bridge, making the spreadsheet state visible while the app is being assembled. Generated dist/app-meta.json also writes a static stateBindings map with access, role, component lineage, and suggested uses. GRID_APP.md includes the same Semantic Bindings section so a custom UI handoff can preserve the model-state intent even when it no longer has the live workbench samples. The same handoff guide includes Capability Review from dist/app-meta.json, including scope evidence, so custom UI authors can see what the Builder app was allowed to read, write, route, run, or call before replacing the generated entry point. The same panel surfaces the custom UI handoff files: GRID_APP.md, the custom-ui/ project folder, custom-ui/manifest.json, custom-ui/README.md, custom-ui/index.html, custom-ui/main.jsx, custom-ui/vue-main.js, custom-ui/grid-app.d.ts, grid-app.js, grid-app.d.ts, grid-react.js, grid-react.d.ts, grid-vue.js, grid-vue.d.ts, custom-app-starter.js, custom-react-starter.jsx, custom-vue-composable.js, grid-connector.js, grid-connector.d.ts, and the createGridState helper. The Navigation block is the first page-aware component in that framework: it renders the App document's pages as tabs, pills, or a list, marks the active page, and routes through the same app-path connector used by route buttons. The builder also offers an App Shell starter from the palette and Pages panel: it creates a routed Dashboard, Records, and Inputs page set from the model's live symbols, with Navigation already placed on each page and responsive sections/grids seeded for dashboards, forms, and records. When the model also has obvious selected-row state for the row source, the shell adds a Record detail page and seeds row cards with Open buttons that write the selected row before routing. Records workflows use the Sidebar layout when selection/detail state is available, keeping filters/detail beside the main table on wide frames and stacking naturally on smaller frames. When the model exposes obvious error, warning, status, result, or validation message cells, generated starters add Alerts and keep those cells out of editable input forms. It is meant to turn a blank App surface into a working multi-page application skeleton in one move. The Bar chart block gives dashboards a first visual primitive over row data: it binds to a row source, infers label/value columns by default, and can be inserted directly from the From model palette for a selected range. Tables can participate in the same app-state loop: a Table may bind a selected value to a model cell and optionally name the row column to write when the user clicks a row. The selected value is read back to highlight the active row, and published/ejected apps preserve that read/write contract. Tables may also bind filter to a text-like model cell; the table filters rows from that value while preserving source-row identity for selection. Authors can pair a Text input and Table on the same search cell, so search is still model state rather than private component state. The Detail block completes the basic master-detail pattern: it reads the same row source and selected value, finds the active row, and renders selected-row fields without requiring an author to build helper formulas first. The Records starter uses this convention when the model already exposes an obvious selection symbol such as SelectedOrder: it creates a selectable table and selected-record detail instead of leaving the author to wire the record workflow by hand. When it also sees an obvious search symbol such as SearchOrders, it adds the search input and binds the table filter automatically. The same workflow is available from the From model palette: choosing Records view on a row source inserts the searchable, selectable, master-detail block for that specific data source. Row sources can also become end-user card lists. Choosing Cards on a row source inserts a Repeater bound to that source; when the builder can see a sample row it seeds a card template with row-scope bindings such as @order.id and @order.amount, and otherwise falls back to automatic labelled cards at runtime. When the model exposes a matching search state such as SearchOrders, the generated Cards workflow includes a search input and binds the Repeater's filter slot to the same model cell. Scalar model state has a matching workflow primitive. Choosing Input form from a number, text, or boolean symbol inserts a Form whose children are model-bound inputs for the detected editable scalar state. Obvious record workflow state, such as SearchOrders and SelectedOrder, stays with the Records view instead of being pulled into the generic Inputs screen. Boolean state can also become reactive structure: choosing Conditional section on a boolean symbol inserts a Card whose visibility is driven by that model cell. This makes show/hide behavior another declarative edge in the App document rather than private component state. Text state can become a view state machine. Choosing Mode switcher on a string-like symbol such as Scenario, Status, or Mode inserts a bound segmented control plus conditional Cards for the inferred options, so an author can build scenario, status, or tab-style app flows from one model cell.

Component

A Component surface is authored JSX inside the workbook. It renders through React and receives a curated model API for reading and writing bound model state.

Use Component when the interface needs code-level control: conditional layouts, custom interactions, richer components, specialized visualizations, or a UI pattern from the React ecosystem. Component surfaces should declare the symbols they read and the symbols they are allowed to write.

One Surface Framework

Built-in and custom surfaces use the same model-facing shape. A surface is a namespace with named slots:

Map_1!type = "map"
Map_1!config = """
version = 1
kind = "map"
title = "Stores"
"""
Map_1!data = ""

The !type slot chooses the surface family. The !config slot stores the surface settings. The !data slot stores surface-owned state such as board cards, layout tiles, app-builder trees, or diagram positions. Some surfaces also use !source for JSX source.

This shared shape is what lets Grid unify built-in product surfaces with custom UI work:

  • A built-in Table, Chart, Map, or Document can start with a native Grid UI.
  • Ejectable built-ins can generate JSX source for customization.
  • A Component can use the same model bindings as an ejected built-in surface.
  • An App can be assembled visually, previewed, and ejected into a Component when the interface needs code-level control.

The result is a gradual path: start with a built-in surface, customize it when the model outgrows the default UI, and keep everything inside the workbook.

Model Bindings

Surfaces bind to model values by name. A Table might bind to Orders; a Chart might bind to MonthlyRevenue; a Map layer might bind to StoreLocations. Bound surfaces update when the model updates, so formulas, connector data, and surface views stay together.

Some surfaces are also editable: moving a Kanban card or dragging a Calendar event writes the change back to the bound cells through the model's normal input path. The model stays the source of truth — the surface is just a faster way to edit it.

Custom surfaces use the same idea. A Component can read a value:

function App() {
  const revenue = model.useValue("Revenue");
  return <ui.Metric label="Revenue" value={revenue} format="currency" />;
}

render(<App />);

It can also read rows:

function App() {
  const orders = model.useRows("Orders");
  return <ui.Table columns={orders.columns} rows={orders.rows} empty="No orders yet." />;
}

render(<App />);

Writes are explicit. Custom surfaces declare which symbols they can write, and Grid blocks writes outside that allowed set. This keeps a custom UI from accidentally editing unrelated parts of the model.

The custom UI model exposes these common reads and writes:

API Purpose
model.get("Name") Read a scalar once.
model.useValue("Name") Reactively read a scalar and rerender when it changes.
model.useValues(["A", "B"]) Reactively read several symbols.
model.useRows("Orders") Read row data with status, columns, and rows.
model.useRange("SomeRange") Read a range-like binding as rows.
model.set("Name", value) Write a value to an allowed symbol.
model.setFormula("Name", formula) Write a formula to an allowed symbol when formula writes are available.

The injected ui primitives provide standard controls and display pieces so a custom surface can look and behave like the rest of Grid without rebuilding basic widgets every time. Current primitives include ui.Stack, ui.Row, ui.Card, ui.Panel, ui.Toolbar, ui.Text, ui.Badge, ui.StatusBadge, ui.Alert, ui.Button, ui.CommandButton, ui.Metric, ui.Field, ui.FilterBar, ui.TextInput, ui.NumberInput, ui.InlineEditor, ui.Select, ui.Toggle, ui.Slider, ui.Table, ui.DataTable, ui.Cards, ui.EmptyState, ui.Progress, ui.KpiGrid, ui.Tabs, ui.Sparkline, and ui.Chart. The framework views — ui.DataTable and ui.Cards — window large row sets to O(visible) DOM and carry the model-state view conventions (bind filter to a search cell; selected/selectKey/onSelect write the selected row back to a model cell), the same conventions the App builder's Table uses. ui.Chart renders an inline SVG chart with no chart library in the bundle. ui.Alert is the feedback banner: compute validation, results, or status in the spreadsheet layer and show it with <ui.Alert when={errors} tone="danger" message={errors} /> — feedback stays model state, gated like the App builder's visibleWhen. A Component can also use standard JSX elements such as div, section, input, select, table, and svg. Package components from the wider React ecosystem must be made available by the host surface scope before a Component can reference them by name.

Authoring Custom UI

When creating a custom UI, start from the user's workflow rather than from the component tree. Decide:

  1. Which model symbols does the UI read?
  2. Which model symbols may it write?
  3. Which state belongs in formulas, and which state belongs in the surface's !data slot?
  4. Is a built-in surface close enough, or should the UI be an App or Component?

A small Component surface has three parts:

Component_1!type = "component"
Component_1!config = """
version = 1
kind = "component"
title = "Revenue Console"

[bindings]
reads = ["Revenue", "Orders"]
writes = ["Scenario"]
"""
jsx Component_1!source = <jsx>
function App() {
  const revenue = model.useValue("Revenue");
  const orders = model.useRows("Orders");

  function applyScenario(next) {
    model.set("Scenario", next);
  }

  return (
    <ui.Stack gap={12}>
      <ui.Metric label="Revenue" value={revenue} format="currency" />
      <ui.Row>
        {["bear", "base", "bull"].map((scenario) => (
          <ui.Button key={scenario} onClick={() => applyScenario(scenario)}>
            {scenario}
          </ui.Button>
        ))}
      </ui.Row>
      <ui.Table columns={orders.columns} rows={orders.rows} empty="No orders yet." />
    </ui.Stack>
  );
}

render(<App />);
</jsx>

The create menu and Component editor also provide scaffold actions for common custom UI starting points: Dashboard, Records, Controls, Scenario, Review, and Kanban. Creating from a scaffold seeds the new Component surface with both JSX source and config; applying a scaffold inside an existing Component replaces the JSX source and rewrites the config in one step. In both cases, the generated UI and its declared read/write bindings stay together. This is the same packaging an AI agent should produce when it creates a custom UI directly: !type = "component", !config with narrow bindings.reads / bindings.writes, and runnable JSX in !source.

AI Agent Contract For Component Surfaces

When an AI agent creates a custom UI, it should produce a complete Component surface package in one edit:

  1. Keep durable business logic in model formulas, rules, or named data values.
  2. Choose a Component namespace, such as ScenarioConsole.
  3. Write ScenarioConsole!type = "component".
  4. Write ScenarioConsole!config with version = 1, kind = "component", a human title, and [bindings].
  5. Put every symbol read by JSX in bindings.reads.
  6. Put only the symbols the UI may mutate in bindings.writes.
  7. Write runnable JSX in jsx ScenarioConsole!source = <jsx>...</jsx>.
  8. End the source with render(<App />); or an equivalent single render call.

Do not make the Component calculate values that should be shared with sheets, built-in surfaces, or other custom UIs. Put those calculations in the model and read them from the Component. Do not write broad allowlists such as every model symbol; writes are a capability boundary.

The authoring surface statically scans JSX and shows repair actions when a symbol is read but missing from bindings.reads, or written but missing from bindings.writes. Those diagnostics are advisory; runtime write enforcement still happens through the Component model API, so undeclared writes are blocked. The editor also exposes authoring metadata for available model symbols, the supported model.* methods, and the blessed ui.* primitives; agents should treat that metadata as the autocomplete contract for generated Component code.

Deterministic Generation Recipe

Agents should use the same planning rules as the Component editor's recommended starter:

  1. Classify the request from intent words first:
    • scenario / planner / forecast / assumption -> Scenario
    • review / approval / queue / triage -> Review
    • kanban / board / pipeline / stage / status -> Kanban
    • record / search / browser / detail / list -> Records
    • control / form / input / edit / settings -> Controls
    • dashboard / KPI / metric / summary / overview -> Dashboard
  2. If intent is vague, infer from model shape:
    • row data plus scalar metrics -> Dashboard
    • row data only -> Records
    • scalar values only -> Controls
    • status/stage-like row data -> Kanban
    • scenario/target/assumption-like scalar values -> Scenario
  3. Generate the Component scaffold package: !type, !config, and !source.
  4. Run the static binding scan over JSX and compare it to [bindings].
  5. Repair missing reads/writes before presenting the result.
  6. Verify the custom UI in Mock, Empty, Loading, and Error preview modes. A generated Component is not done until those states render cleanly.

The planner is exposed in code as buildComponentGenerationPlan. It returns the chosen scaffold, generated package, reads, writes, validation checks, safety notes, and rationale. Agents should prefer that helper when they are operating inside the Grid UI codebase; otherwise they should follow the recipe above exactly.

The injected model API has this shape:

interface ComponentModelApi {
  get(symbol: string): unknown;
  useValue(symbol: string): unknown;
  useValues(symbols: readonly string[]): Record<string, unknown>;
  useRows(symbol: string): {
    status: "unbound" | "loading" | "error" | "empty" | "bound";
    columns: Array<{ key: string; label: string; type?: string }>;
    rows: Array<Record<string, unknown>>;
    label: string;
    error?: string;
  };
  useRange(symbol: string): Array<Record<string, unknown>>;
  set(symbol: string, value: unknown, typeTag?: string): void;
  setFormula(symbol: string, formula: string): void;
}

The injected ui primitives are React components. They are intentionally small and composable:

ui.Stack;       // vertical layout
ui.Row;         // horizontal layout
ui.Card;        // framed content
ui.Panel;       // titled framed section
ui.Toolbar;     // header/actions row
ui.Text;        // muted/supporting text
ui.Badge;       // status token
ui.StatusBadge; // tonal status token
ui.Button;      // action button
ui.CommandButton; // primary/quiet/danger command
ui.Metric;      // labeled scalar
ui.Field;       // label/value display
ui.FilterBar;   // search/filter row
ui.TextInput;   // string write control
ui.NumberInput; // numeric write control
ui.InlineEditor; // compact editable value
ui.Select;      // option write control
ui.Toggle;      // boolean write control
ui.Slider;      // numeric range control
ui.Table;       // row display
ui.DataTable;   // windowed data view: any row count, filter + selection write-back
ui.Cards;       // windowed card list over rows (renderCard) with filter
ui.StatusBadge; // tonal status token
ui.Alert;       // feedback banner, gated by model state (when)
ui.EmptyState;  // empty/error panel
ui.Progress;    // progress meter
ui.KpiGrid;     // repeated metrics
ui.Tabs;        // segmented sections
ui.Sparkline;   // compact trend chart
ui.Chart;       // dependency-free SVG chart (line/bar/area) over rows

For a model like this:

Revenue is currency = 720000
Scenario = "base"
Orders = [
  { id: "ORD-1", customer: "Acme", amount: 42000 },
  { id: "ORD-2", customer: "Northstar", amount: 88000 }
]

an agent-generated Component should look like this:

ScenarioConsole!type = "component"
ScenarioConsole!config = """
version = 1
kind = "component"
title = "Scenario Console"

[bindings]
reads = ["Revenue", "Scenario", "Orders"]
writes = ["Scenario"]
"""
jsx ScenarioConsole!source = <jsx>
function App() {
  const revenue = model.useValue("Revenue");
  const scenario = String(model.useValue("Scenario") ?? "base");
  const orders = model.useRows("Orders");

  return (
    <ui.Stack gap={12}>
      <ui.KpiGrid
        items={[
          { label: "Revenue", value: revenue, format: "currency" },
          { label: "Scenario", value: scenario }
        ]}
      />
      <ui.Select
        label="Scenario"
        value={scenario}
        options={["bear", "base", "bull"]}
        onChange={(next) => model.set("Scenario", next)}
      />
      <ui.Table columns={orders.columns} rows={orders.rows} empty="No orders yet." />
    </ui.Stack>
  );
}

render(<App />);
</jsx>

Keep calculations in formulas whenever possible. Use the custom UI for interaction, presentation, and workflow. This keeps the model inspectable and lets the same values continue to work in sheets, built-in surfaces, and custom surfaces.

Complete Surface Examples

These examples show the model-source shape a user or AI agent should produce. They are intentionally small, but each one includes both model data and surface slots.

CRM With Table And Kanban

MODEL "Pipeline CRM"
DESCRIPTION "Track accounts as rows, then show them as a table and workflow board."
VERSION "1.0.0"
AUTHOR "AI Agent"
TAGS "surface-template", "crm", "table", "kanban", "surfaces"

Accounts = [
  { name: "Acme", owner: "Mina", stage: "lead", value: 42000 },
  { name: "Northstar", owner: "Owen", stage: "qualified", value: 88000 },
  { name: "River Co", owner: "June", stage: "proposal", value: 56000 }
]

TotalPipeline IS currency = SUM(MAP(Accounts, row => row.value))

Accounts_Table!type = "table"
Accounts_Table!config = """
version = 1
kind = "table"
title = "Accounts"

[columns]
bind = "Accounts"
title = "Accounts"

[[columns.fields]]
key = "name"
label = "Account"

[[columns.fields]]
key = "owner"
label = "Owner"

[[columns.fields]]
key = "stage"
label = "Stage"

[[columns.fields]]
key = "value"
label = "Value"
"""
Accounts_Table!data = ""

Pipeline_Board!type = "kanban"
Pipeline_Board!config = """
version = 1
kind = "kanban"
title = "Pipeline"

[layout]
wip_limit = 8

[[columns]]
id = "lead"
title = "Lead"

[[columns]]
id = "qualified"
title = "Qualified"

[[columns]]
id = "proposal"
title = "Proposal"

[items]
bind = "Accounts"
column = "stage"
title = "name"
assignee = "owner"
"""
Pipeline_Board!data = ""

END MODEL

Store Map

MODEL "Store Map"
DESCRIPTION "Show store locations on a map."
VERSION "1.0.0"
AUTHOR "AI Agent"
TAGS "surface-template", "map", "locations", "surfaces"

Stores = [
  { id: "SFO", name: "San Francisco", latitude: 37.7749, longitude: -122.4194, revenue: 125000 },
  { id: "OAK", name: "Oakland", latitude: 37.8044, longitude: -122.2712, revenue: 92000 },
  { id: "SJC", name: "San Jose", latitude: 37.3382, longitude: -121.8863, revenue: 111000 }
]

TotalRevenue IS currency = SUM(MAP(Stores, store => store.revenue))

Store_Map!type = "map"
Store_Map!config = """
version = 1
kind = "map"
title = "Store Map"

[layout]
basemap = "streets"
center = [37.7749, -122.4194]
zoom = 9

[[layers]]
name = "Stores"
bind = "Stores"
lat = "latitude"
lng = "longitude"
label = "name"
color = "#2563eb"
"""
Store_Map!data = ""

END MODEL

KPI Layout Dashboard

MODEL "Revenue Dashboard"
DESCRIPTION "Summarize a forecast in dashboard tiles."
VERSION "1.0.0"
AUTHOR "AI Agent"
TAGS "surface-template", "dashboard", "layout", "surfaces"

MonthlyRevenue = [120000, 135000, 142000, 150000, 168000]
Pipeline = 186000
Forecast IS currency = SUM(MonthlyRevenue#) + Pipeline
AverageMonth IS currency = AVERAGE(MonthlyRevenue#)

Dashboard!type = "layout"
Dashboard!config = """
version = 1
kind = "layout"
title = "Dashboard"

[layout]
cols = 12
rows = 6
show_controls = true

[[tiles.static]]
title = "Forecast"
x = 0
y = 0
w = 4
h = 2
formula = "=Forecast"

[[tiles.static]]
title = "Average Month"
x = 4
y = 0
w = 4
h = 2
formula = "=AverageMonth"

[[tiles.static]]
title = "Notes"
x = 0
y = 2
w = 12
h = 4
body = "Forecast includes five months of actuals plus open pipeline."
"""
Dashboard!data = ""

END MODEL

Custom Scenario Console

MODEL "Scenario Console"
DESCRIPTION "A custom Component surface reads model values and writes the selected scenario."
VERSION "1.0.0"
AUTHOR "AI Agent"
TAGS "surface-template", "custom-ui", "component", "surfaces"

Revenue IS currency = 720000
Scenario = "base"
Orders = [
  { id: "ORD-1", customer: "Acme", amount: 42000 },
  { id: "ORD-2", customer: "Northstar", amount: 88000 },
  { id: "ORD-3", customer: "River Co", amount: 56000 }
]

ScenarioConsole!type = "component"
ScenarioConsole!config = """
version = 1
kind = "component"
title = "Scenario Console"

[bindings]
reads = ["Revenue", "Scenario", "Orders"]
writes = ["Scenario"]
"""
jsx ScenarioConsole!source = <jsx>
function App() {
  const revenue = model.useValue("Revenue");
  const scenario = model.useValue("Scenario");
  const orders = model.useRows("Orders");

  return (
    <ui.Stack gap={12}>
      <ui.Metric label="Revenue" value={revenue} format="currency" />
      <ui.Select
        label="Scenario"
        value={String(scenario ?? "")}
        options={["bear", "base", "bull"]}
        onChange={(next) => model.set("Scenario", next)}
      />
      <ui.Table columns={orders.columns} rows={orders.rows} empty="No orders yet." />
    </ui.Stack>
  );
}

render(<App />);
</jsx>

END MODEL

Choosing A Surface

Use a built-in surface when the model fits a known workflow: tables for records, maps for locations, boards for planning, documents for narrative, charts for analysis, and layouts for dashboards.

Use an App surface when you want to compose a model-bound screen without writing React first.

Use a Component surface when you want a custom front end with React-level control over layout, state, components, and interaction.

Use eject when a built-in surface gets you most of the way there but the final experience needs custom behavior. Ejecting keeps the model bindings and produces editable JSX source that can continue inside the workbook.

Current Boundary

Surfaces are a UI layer over model state. They do not replace formulas, rules, or the source editor. The model remains the durable computation layer; surfaces are how people operate, inspect, and present that computation.

That boundary is intentional. It lets Grid be a spreadsheet, a model, and an application shell at once: built-in surfaces provide familiar app-like interfaces, and custom surfaces let teams build the exact UI their model needs.