← All guided builds

Guided build · 17

Turn a model into an app

Extend one CRM model with Table, Kanban, and Layout, then compare Map and custom Component patterns honestly.

You will finish with: A working multi-surface CRM plus a pattern map for choosing built-in views and trusted custom UI.

25 minIntermediateSource checked for Grid 0.61.0Reviewed 2026-08-25
Related canonical example10-surface-pipeline-crm.grid
Get Grid
Starter modelpipeline-crm-starter.gridThe account row source and total before Table, Kanban, and Layout surfaces are declared.
Completed solutionpipeline-crm-solution.gridThe finished CRM with Table, Kanban, and Layout surfaces over one reactive model.
Writable Component examplescenario-console-writable.gridA corrected Scenario Console whose allowlisted write targets a source-owned input.

Watch it in Grid

See the workflow before you build it.

Follow the finished interaction, then use the written steps below to build and inspect it yourself.

Companion film

The model is the app

Declare data, logic, and interface together, then open the same model as a working pipeline board.

16 secGrid 0.61.0
Open film page
On this page

What you will build

One reactive CRM model presented through Table, Kanban, and Layout, followed by a comparison with the Map and custom Component patterns in the neighboring canonical examples.

The working build stays in the Pipeline CRM file. Use the downloadable Completed solution to compare your final Table, Kanban, and Layout declarations. The other canonical examples are an architectural tour, not files to paste together: surfaces remain views over named model bindings, and write access still follows the language ownership contract.

1. Keep the model as the source of truth

Start with the Pipeline CRM example:

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))

Checkpoint: Accounts contains three rows and TotalPipeline = 186,000.

Everything that follows binds to these symbols. Do not copy the pipeline calculation into a surface configuration.

2. Show the same rows two ways

The Table surface maps record fields to columns. The Kanban surface maps stage to columns and uses name and owner for cards.

Both configurations point at Accounts:

[columns]
bind = "Accounts"

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

Checkpoint: the Table shows three rows; the board shows one card in Lead, Qualified, and Proposal. The model still owns one Accounts value.

Editable writeback is available only when the bound source accepts writes. A visual drag must not invent permission to mutate a read-only value.

3. Add a geographic view

Open the Store Map example and inspect its binding pattern:

[[layers]]
name = "Stores"
bind = "Stores"
lat = "latitude"
lng = "longitude"
label = "name"

The map renderer handles presentation. Coordinates and revenue remain ordinary model data.

Checkpoint: three Bay Area locations render and TotalRevenue = 328,000.

4. Turn model outputs into a dashboard

The Revenue Dashboard example binds Layout tiles to named formulas:

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

Checkpoint: Forecast = 901,000 and AverageMonth = 143,000. The Layout shows both without reimplementing either formula.

Change a monthly value and watch the formula update first; the tile follows the binding.

5. Inspect a narrowly scoped custom screen

The Scenario Console declares exactly what its Component may read and write:

[bindings]
reads = ["Revenue", "Scenario", "Orders"]
writes = ["Scenario"]

Inside the Component, injected helpers subscribe to model values. The configuration also declares an intended write allowlist:

const revenue = model.useValue("Revenue");
const scenario = model.useValue("Scenario");
const orders = model.useRows("Orders");

<ui.Select
  label="Scenario"
  value={String(scenario ?? "")}
  options={["bear", "base", "bull"]}
  onChange={(next) => model.set("Scenario", next)}
/>

Checkpoint: the screen shows revenue 720,000, scenario base, and three orders.

There is an important 0.61.0 ownership caveat in this exported canonical example: Scenario = "base" is a formula-owned binding, not an input. The writes = ["Scenario"] allowlist narrows what a Component may request, but it does not make a formula-owned symbol externally writable. Under the documented contract, the select write is rejected. Treat the exported screen as a read and permission-boundary inspection, not a working scenario selector.

To test the interaction, open the downloadable Writable Component example instead. Its source owns the target explicitly:

input Scenario = "base"

Its Component keeps the same narrow writes = ["Scenario"] declaration, so both required layers agree: the model owns a writable input and the UI is allowlisted to request that one write. This is a site-owned corrective practice artifact; it does not alter or silently replace the exported canonical example.

Custom Component execution requires a Grid environment that permits trusted custom UI. Built-in surfaces remain the simpler lower-trust path.

6. Add a dashboard to the CRM model

Return to the Pipeline CRM source and add two named results before END MODEL:

UpsideMultiplier IS percentage = 10pct
UpsidePipeline IS currency = TotalPipeline * (1 + UpsideMultiplier)

Then add a Layout surface with two static tiles whose formulas are =TotalPipeline and =UpsidePipeline. Give it a unique namespace such as Pipeline_Summary so it can coexist with Accounts_Table and Pipeline_Board.

Checkpoint: the CRM now has three surfaces over one formula layer. Total Pipeline is 186,000 and Upside Pipeline is 204,600.

Try it yourself

Change Acme's authored value from 42,000 to 52,000. The Table and Kanban still show the same three records, while TotalPipeline becomes 196,000 and UpsidePipeline becomes 215,600. Add a third Layout tile bound to UpsideMultiplier. Keep every calculation in Grid formulas rather than surface configuration.

You are done when

  • Table, Kanban, and Layout read the same CRM model.
  • Calculations stay in the model rather than UI configuration.
  • A shared source change propagates to every bound surface.
  • You can explain why a Component write allowlist does not override binding ownership.

Next, follow Build your first Grid app for the complete App Builder path: routed pages, guarded actions, read-only Preview, capability review, App Mode, and a packaged App Home.

Build statusReached the expected checkpoint?