> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parable.work/llms.txt
> Use this file to discover all available pages before exploring further.

# Lab Parables

> Run an imported React app as a Lab Parable and keep it working in the Workspace.

# Lab Parables

A Lab Parable is a Parable whose app is an imported React package, called a Lab. It exists for teams that already have a React app, or need full control of their screens: they upload the app as one ZIP instead of rebuilding it from Pages and Panes. Parable supplies React, the shared design system, the reader's Workspace data, and the publishing lifecycle, so the app reaches customer Workspaces with the same review and access checks as any other Parable. A Lab does not need Parable Pros: LAB and the Parable Pros attribution are separate choices, and either can be on without the other.

The ZIP holds the app's code, pages, styles, and assets; an app with twenty pages is still one package. Its entry file, src/App.tsx or App.tsx (or the .jsx equivalents), exports a React component, and Parable mounts it, so the app never creates its own React root. Another entry path needs a parable.lab.json file at the ZIP root that names it. Leave out node\_modules, React, the design system, .env files, and .git. Import never runs install or build scripts, so any extra browser library must already be bundled into the app's files. The app reads data with the query function from the parable module, which runs SQL in the reader's Workspace: Provider tables are named `providers.<connector>.<table>` and published Artifacts are named `artifacts.<table>`. Values go in as bound parameters, never pasted into the SQL text. A reader with a Peeking seat cannot read Provider or Artifact tables with query, so readers get data through the Plots that parable.lab.json declares: readPlot("capacity") returns that Plot's rows, and query over plotTable("capacity") filters it with bound parameters, as in `SELECT * FROM ${plotTable("capacity")} WHERE team = $1`. Both refuse a slug that parable.lab.json does not declare. The app holds no credentials, database connections, or Provider SDKs, and it cannot choose another Workspace or person. Shared controls import from parable/panes/placeholders; the host also supplies Motion, Kysely, scalar-lib, and D3.

SQL that the app runs reads every pool only for an editor of the Parable. A reader with a Peeking seat, such as an Informed Viewer, runs SQL in a Parable only over its Plots, so app SQL that names `providers.`, `artifacts.` or `workspace.` gets a refusal for that reader. Declare that SQL as Plots: put each query in its own .sql file in the ZIP, and list it under `plots` in parable.lab.json with a `slug`, a `name` and the file path in `sql`. The import keeps each one as a Plot on the Lab's data Pane, a Pane that draws nothing. The app then reads the Plot with readPlot, or with query over plotTable, and every reader with a seat can run that read. A Plot's rows, its Artifact and Workspace rows included, show to every reader with a seat. A Plot returns text, numbers, true or false, JSON, and lists or objects of them, and it takes no parameters: filter its rows in the app's own SQL over plotTable with bound parameters. The import confirmation names each line of app source whose SQL names one of those pools. A warning does not block the import. A Plot whose SQL check did not finish at import, as when the Query Layer was not ready, is checked again on an editor's next read.

The rules that matter: a Lab has no structured Pieces or Preferences that it draws, so filters and navigation live in React state and navigation stays inside the app. Query results are displayed in the app; a Lab does not save them as Reports. The Parable Pros, LAB, and package controls each have separate view and modify permissions, and reading a deployed Lab needs none of them. A reader without Ponder permission or the Lab package view permission, such as a Viewer, sees the app alone, with no Files and no Collaboration. A Lab opens with Ponder's column hidden. The app runs in the reader's browser, so every file in the ZIP reaches that browser, data files included: keep private data out of the ZIP and read it through Plots. Every query still passes the reader's Workspace access and data checks. Each destination Workspace needs its own Provider connections or Artifacts, because deploying does not copy data. A package is limited to 64 MiB compressed, 256 MiB expanded, 16 MiB per file, and 20,000 entries; symlinks, path traversal, and colliding paths are refused. A nonempty Lab cannot become a structured Parable; start from an empty Parable for that.

1. Open a new or empty Parable, check LAB, choose Import Lab Package, and upload the ZIP. Turn on Parable Pros as well only when Parable Pros built the app.
2. Before writing queries, ask Ponder to discover the exact table and column names in the Workspace's data catalog. Read current rows from the whole table where \_parable\_deleted is false and \_parable\_observed is true; the latest dt holds only that day's changed rows.
3. Preview the app as an active member of the target Workspace so its queries return that Workspace's data.
4. In a Personal Parable, the import saves directly. In Publishing, the upload opens or selects your pending Changes: preview, propose the Changes, publish a Version, and deploy it. In Publishing, a Lab also needs a description and a preview to publish. The package does not carry them, so ask Ponder to make them.
5. To update the app, change the files locally and use the upload icon beside LAB to import a replacement ZIP. Parable replaces the whole package. A Plot keeps its slug across imports: a changed .sql file or name updates the Plot, and a Plot that parable.lab.json no longer lists is deleted. In Publishing, pending Changes do not replace the published app; readers receive the replacement after publication and delivery.
6. If an import fails, the previous package stays in place. Fix the file or import named in the error and upload again.

Ponder changes a Lab package with plan\_lab\_package\_edit and then apply\_lab\_package\_edit. An edit sends the whole new text of each file that it changes, by package path, or null to delete that file. It also sends the Plot declarations of parable.lab.json: null keeps them, and a list replaces them. An edit carries text only. An image or another binary file changes through a ZIP import. The plan runs the planner that a ZIP import runs. It returns the changes, the diagnostics, and the warnings, with a base version and a plan digest. A warning names a file and a line where the app sends SQL to `providers.`, `artifacts.`, or `workspace.` tables. A reader with a Peeking seat gets a refusal there, because a reader's SQL in a Parable reads only that Parable's Plots. Resolve each warning:

1. Move the SQL into `plots/<slug>.sql` and declare that file in the Plot declarations with its slug and name.
2. Replace the call with `readPlot(slug)`. When the app filters the rows with bound parameters, keep `query()` and read from `plotTable(slug)` in its SQL.
3. Plan the edit again. Do this until no warning is left.
4. Apply the edit with the plan's base version as expectedVersion and its plan digest. If apply refuses because the Lab or the plan changed, plan again. In Publishing, pass the proposalId of your own open Changes.

A good Lab renders every page in preview, shows loading, empty, error, and retry states, and returns real rows for an active member. Images and other assets are imported in code, because a literal path such as /logo.svg has no server inside the sandbox. To check the result, click through each page, run each query, confirm a reader in a receiving Workspace can open the app in Peeking, and confirm the data shown belongs to that Workspace.

Related Powers: Pro Parables, Providers, Publishing, Workspaces. Find each by its title; do not guess stored names.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.