CLI reference
How create-grist-widget scaffolds a project, starts from a real example, and what the bundled GitHub Pages deploy pipeline does.
Getting started here covers the CLI's main commands and scaffolding a new project from a terminal, CLI-only. If you'd rather copy the template repo on GitHub, set up an AI coding agent, or connect a deployed widget to a real Grist document, see the Quickstart — both paths produce identical code.
npm create grist-widget
npm create grist-widget my-widget
cd my-widget
pnpm devThe template — and every starting example, and the logic that composes them — are embedded directly in the CLI. Nothing is fetched from GitHub or npm at scaffold time; the dependency install right after is the only network access any of this makes. Scaffolding via the CLI is the same starting point as grist-widget-template (what the Quickstart's template-copy path gives you), so both paths converge on identical code.
By default the scaffold is blank — a two-field editor — and its
dependencies install automatically (the package manager is detected from
how you invoked the command). Run npm create grist-widget with no name
for an interactive prompt instead, which asks for both a name and a
starting example.
What you get
index.htmlhas thegrist-plugin-api.jsscript tag wired in.src/main.tsxmounts the React root.src/App.tsxshows the provider + boundary +useGrist()pattern, with placeholder UI you can immediately replace — or, with--example, a real starting widget instead of the placeholder.src/App.test.tsxis a working test against the placeholder, usingrenderWithGrist— run it, then edit it alongsideApp.tsx.- Tailwind preconfigured for theme-aware widgets (light / dark / system).
- Prettier + ESLint with project-wide rules.
After scaffolding, the first edit is typically src/App.tsx — replace the
placeholder with the widget you want. The
Cookbook has ten ready-to-paste recipes for
common shapes, and create-grist-widget list shows every example the CLI
itself can start you from directly (see below).
The four commands
| Command | What it does |
|---|---|
create <name> [--example <id>] | Scaffold a new widget into ./<name> — same as npm create grist-widget <name> |
init [--example <id>] | Scaffold into the current directory instead of a new one |
use <example-id> | Swap the current project's widget for a different example |
list [--json] | Print the available starting examples |
Common flags for create/init: --yes (skip interactive prompts),
--no-install (skip the automatic dependency install), --pm <name>
(force a specific package manager). init also takes --name <name>
(default: the current directory's name) and --force (see below).
Starting from a real example
npm create grist-widget my-widget --example form-edit
# or interactively:
npm create grist-widgetcreate-grist-widget list prints every available id with a short
description; list --json gives the same as machine-readable JSON. Every
row below is exactly what list returns, generated from the same manifest
the CLI itself embeds — the id is what --example <id> / use <id> take,
and the same slug shown in its playground URL (/examples/<id>/latest/).
| Example | Description | Use |
|---|---|---|
| Attachment gallery | Thumbnail grid of every attachment in the selected row. Showcases extractGristAttachmentIdsFromCell + useGrist().fetchAttachmentBlob. | attachment-gallery |
| Bulk mark done | Bulk table.update with an array of row patches. | bulk-mark-done |
| Declared columns | Shows GRIST_OPTIONS column declarations and mappedRecord after the user maps columns. | declared-columns |
| Row editor | Edit the selected row in a polished form. Showcases column mappings, write status, and error UI. | form-edit |
| hello-world | hello-world | |
| Mark done | Single-row write via table.update. | mark-done |
| Reads demo | listTables, fetchTableRows, fetchRow, fetchSelectedRecord. | reads-demo |
| REST fetch | fetchWithAuth against the Grist REST API. | rest-fetch |
| Safe parse | safeParseGristTableData with per-cell issues. | safe-parse |
| Schema migration | applyActions with gristAddVisibleColumnAction, gristRenameColumnAction, and gristRemoveColumnAction. | schema-migration |
| Schema preview | useGristSchema with replicaRowMode schema+samples for LLM context. | schema-preview |
| Slice hooks | useGristStatus, useGristSelection, useGristWrites, useGristTheme in separate child components. | slice-hooks |
| Spreadsheet grid | Selected Grist section rendered as a fully-synced, editable spreadsheet via react-datasheet-grid, plus a schema toolbar and replica-document export. | spreadsheet-grid |
| Task board | Mini Kanban board grouped by Status. Drag a card to update Status; click to focus the row in linked sections. | task-board |
| Theme | Read w.theme from the host document. | theme-demo |
| Widget options | Persist UI state with setWidgetOption / widgetOptions. | widget-options |
| Table writes | create, update, upsert, destroy via w.table. | writes-demo |
init — scaffolding into an already-initialized directory
create refuses a non-empty target directory. init is the fix for the
case that comes up constantly with cloud coding agents, which typically
start from an already-cloned or already-git init'd repo rather than an
empty one:
mkdir my-widget && cd my-widget
git init # or: this is already a repo clone
npx create-grist-widget init --example form-editFiles that already exist and are identical to the template are left
untouched; the four identity-bearing files (package.json, README.md,
index.html, src/App.tsx) get the usual name/title substitution even if
present. Anything else that already exists and differs from the
template's own copy stops the run and lists the conflicting paths — pass
--force to overwrite anyway. If .git already exists, init never
auto-commits; review and commit the result yourself. If it doesn't, init
sets up main + dev and an initial commit exactly like create does.
use — swapping to a different example later
create-grist-widget use task-boardSwaps src/App.tsx (and any example-specific files) for a different
example, in place. It only ever touches src/** — never main.tsx,
vite.config.ts, or the deploy workflow — and it requires a clean git
working tree first, so the swap is always a single reviewable, revertible
diff. Commit or stash first if you have uncommitted changes; use refuses
to run otherwise.
create,init,use, andlistare themselves valid widget names —create-grist-widget listruns the list command, not "scaffold a widget named list." Use the explicit form (create-grist-widget create list) for a widget with one of these names.
The bundled deploy pipeline
Every scaffolded repo ships its own .github/workflows/deploy.yml, two
channels from one workflow:
- Push to
main→ the release channel: an immutable/<repo>/v<version>/plus a mutable/<repo>/latest/(and the same build at the repo's Pages root). Re-pushingmainwithout bumpingpackage.json's version is a no-op — it only rebuilds when a newv<version>doesn't exist yet. - Push to
dev→ the dev channel: a mutable/<repo>/dev/, for live review inside Grist as you iterate.
If you scaffolded via "Use this template"
with Include all branches checked, GitHub Pages is already pointed at
the copied gh-pages branch — nothing else to configure. Scaffolding a
fresh, non-template repo yourself needs two one-time steps instead:
- Settings → Pages → Source: Deploy from a branch →
gh-pages→/(the workflow creates thegh-pagesbranch itself, but Pages has to be pointed at it once — left onmain, you'd see a blank page with a 404 for/src/main.tsxeven though the workflow reports success). - Settings → Actions → General → Workflow permissions → Read and write permissions.
Hand-rolled (no template)
If you want the bare minimum instead of the CLI:
mkdir my-widget && cd my-widget
pnpm init
pnpm add react react-dom grist-widget-sdk
pnpm add -D typescript @types/react @types/react-dom vite @vitejs/plugin-reactThen create index.html (with the plugin-api script tag), src/main.tsx
(React root), and src/App.tsx (your widget) — see the
Cheat sheet for the smallest possible code,
or the Cookbook for ten ready-to-paste
recipes. The template path saves about ten minutes of boilerplate and one
or two configuration gotchas, but the SDK works fine without it.