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

# Build a Lab

> Build a Lab: a React app, packaged as one ZIP, that runs inside a Parable and reads your Workspace's data.

A **Lab** is a React app that runs as the app of a Parable. You build it with
your own tools, package it as one ZIP file, and import it into a Parable.
Parable supplies React, the shared design-system controls, the reader's
Workspace data, and the publishing lifecycle. A Lab therefore reaches its
readers through the same review and access checks as any other Parable.

Use a Lab when your team already has a React app, or needs full control of
its screens. An app with twenty pages is still one package.

> [!NOTE]
>
> Parable gives you a Lab kit: the type declarations of the `parable` module
> and the shared controls, and a tool that packs your app into a ZIP. Ask your
> Parable representative for the Lab kit.

## What a package holds

| Part | Rule |
| - | - |
| Entry file | A file that exports your app's root component as its default export (`export default function App() { ... }`). Parable mounts it. Your app never creates its own React root. |
| `parable.lab.json` | Optional, at the root of the ZIP. It names the entry file and declares the Plots that the app reads. |
| Plot SQL | One `.sql` file for each declared Plot. |
| Source and assets | TypeScript, JavaScript, CSS, JSON, images, fonts, and text files that the app imports. |

Without `parable.lab.json`, Parable looks for `src/App.tsx`, `src/App.jsx`,
`App.tsx`, or `App.jsx`, and refuses a package that has more than one of them.
An entry file ends in `.tsx`, `.ts`, `.jsx`, `.js`, or `.mjs`.

Leave out `node_modules`, `.git`, `.env` files, React, and the design system.
Parable never runs install or build scripts. Bundle any other browser library
into the app's own files. Import images and other assets in code: a literal
path such as `/logo.svg` has no server inside the Lab.

The host supplies these modules, so do not bundle them:

| Module | Version |
| - | - |
| `react`, `react-dom` | 19.3.1 |
| `motion` | 14.2.0 |
| `d3` | 7.9.0 |
| `kysely` | 0.29.6 |
| `three` | 0.186.1 |
| `@psgen/scalar-lib` | The version that the host runs |
| `parable/panes/placeholders` | The shared design-system controls, such as `Button`, `Card`, `DataGrid`, `Select`, `Tabs`, and `toast` |
| `parable` | The Lab SDK, below |

## The manifest

```json parable.lab.json theme={null}
{
  "runtimeVersion": 1,
  "entryPoint": "src/App.tsx",
  "plots": [
    {
      "slug": "capacity",
      "name": "Team capacity",
      "sql": "plots/capacity.sql"
    }
  ]
}
```

| Field | Meaning |
| - | - |
| `runtimeVersion` | The Lab runtime. The only value is `1`, which is also the default. |
| `entryPoint` | The path of the entry file inside the ZIP. |
| `plots` | Optional. The Plots that the app reads. Each has a `slug`, a `name`, and the path of its `.sql` file in `sql`. |

Parable refuses a field that it does not know. A Plot `slug` starts with a
lowercase letter and holds lowercase letters, digits, and single underscores,
such as `weekly_load`. Parable does not change a slug for you: `Weekly-Load`
is refused. `lab_data` is reserved. Each slug and each `.sql` file belongs to
one Plot.

## Read data from the app

Import the SDK from the `parable` module:

```tsx src/App.tsx theme={null}
import { useEffect, useState } from 'react';
import { plotTable, query, readPlot, useParableContext } from 'parable';
import { Card, CardContent, CardHeader, CardTitle } from 'parable/panes/placeholders';

export default function App() {
  const context = useParableContext();
  const [rows, setRows] = useState<Record<string, unknown>[]>([]);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    query(`SELECT team, open_items FROM ${plotTable('capacity')} WHERE team = $1`, ['Platform'])
      .then(result => setRows(result.rows))
      .catch(failure => setError(failure.message));
  }, [context.data.kind]);

  return (
    <Card>
      <CardHeader>
        <CardTitle>Platform capacity</CardTitle>
      </CardHeader>
      <CardContent>{error ?? `${rows.length} rows`}</CardContent>
    </Card>
  );
}
```

| Function | What it does |
| - | - |
| `query(sql, parameters?)` | Runs one read-only SQL statement in the reader's Workspace. `parameters` binds `$1`, `$2`, and so on, by position. A value is a string, a finite number, a boolean, a `Date`, or `null`. |
| `readPlot(slug)` | Returns the rows of a Plot that `parable.lab.json` declares. |
| `plotTable(slug)` | Returns the SQL name of a declared Plot, already quoted, so that `query()` can filter it with bound parameters. It does not call the server. |
| `getParableContext()` | Returns the context the app runs in, once. |
| `useParableContext()` | A React hook that returns the same context and renders again when it changes. |

`query()` and `readPlot()` resolve to the same result:

| Field | Meaning |
| - | - |
| `columns` | Each column's `key` and `label`. |
| `rows` | One object for each row, keyed by column. |
| `rowCount` | The number of rows returned. |
| `truncated` | `true` when the statement returned more than 10,000 rows. The app reads the first 10,000. Write a `LIMIT` or aggregate. |
| `elapsedMs` | How long the read took. |

The context has the Workspace, the Parable and the revision being read, the
data reading (`CURRENT`, or a stored Refresh), the reader, and the reader's
seat. A failed call rejects with an error that has a `message` and a `code`,
such as `HANDLE_UNDECLARED`.

Always put values in `parameters`. Never paste them into the SQL text. The app
holds no credentials, database connections, or Provider SDKs. It cannot choose
another Workspace or another person.

## Give every reader the data

Who can run SQL in a Parable depends on the reader:

* A reader who can edit the Parable reads every pool that their role allows.
  Editing needs the `tenant.parables.manage` permission and a Publishing or
  Primary seat on the Parable.
* Every other reader, such as a reader with a Peeking seat, reads only the
  Parable's own Plots. Their `query()` over a `providers.`, `artifacts.`, or
  `workspace.` table is refused with `403` and `PA-AU-019`.

So put the SQL that reads the pools into declared Plots, and read the Plots
from the app:

1. Put each query in its own `.sql` file, such as `plots/capacity.sql`.
2. Declare it under `plots` in `parable.lab.json`, with a `slug`, a `name`,
   and the file path.
3. In the app, read it with `readPlot('capacity')`. To filter it with bound
   parameters, use `query()` over `plotTable('capacity')`.

Every reader with a seat on the Parable can read a declared Plot. A Plot's
rows, Artifact and Workspace rows included, show to each of them. A Plot
takes no parameters, and returns text, numbers, booleans, JSON, and lists or
objects of them. A slug that `parable.lab.json` does not declare fails with
`HANDLE_UNDECLARED`.

When you import a package, Parable warns you about each line of app source
whose SQL names `providers`, `artifacts`, or `workspace` tables. A warning
does not block the import. Resolve it before you publish, so that every reader
sees data.

## Import the package

1. Open a new or empty Parable in the app. Lab mode can be turned on only for
   an empty Parable.
2. Turn on **LAB**, choose **Import Lab Package**, and upload the ZIP.
3. Preview the app as an active member of the target Workspace, so that its
   queries return that Workspace's data.
4. In a Personal Parable, the import saves directly. In a Parable with
   Published Versions, the import adds to your pending Changes: preview them,
   propose them, publish a Version, and deploy it.

To update the app, change your files and import a replacement ZIP with the
upload icon next to **LAB**. 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. If an import fails,
the previous package stays in place.

### Import over the Workspace API

The app's import calls two operations, which you can also call with a Parable
token:

1. `POST /api/parable/authoring/primaries/{rootRefId}/actions/plan-archive-import`
   checks the ZIP and changes nothing. Send it as `multipart/form-data` with
   the ZIP in `file`. The plan returns `changes`, `diagnostics`, `warnings`,
   `applicable`, `baseVersion`, and `planDigest`.
2. `POST /api/parable/authoring/primaries/{rootRefId}/actions/apply-archive-import`
   applies the plan. Send the same `file`, with `expectedVersion` set to the
   plan's `baseVersion` and the plan's `planDigest`. If the Parable changed
   since the plan, plan again.

`rootRefId` is the Parable's `root_ref_id` in `workspace.parables`. Add
`?proposalId=<id>` to import into your own open Changes. The API refuses an
MCP token here.

## Limits

| Limit | Value |
| - | - |
| ZIP size | 64 MiB |
| Expanded size | 256 MiB |
| One file | 16 MiB |
| `parable.lab.json` | 8 MiB |
| Entries | 20,000 |
| Path length | 4,096 bytes |

Parable also refuses a package with symbolic links, encrypted or ZIP64
entries, absolute paths or `..` in a path, or two paths that differ only by
letter case. It ignores `__MACOSX/` and `.DS_Store`, and it unwraps one
folder that encloses the whole app.

## Permissions

| Action | Permissions |
| - | - |
| Read a deployed Lab | A seat on the Parable. No Lab permission is needed. |
| See or export the package | `tenant.parables.view` and `tenant.parables.labs.packages.view` |
| See the Lab's files and Collaboration in Ponder's column, or open a file from a link | `tenant.parables.ponder` or `tenant.parables.labs.packages.view` |
| Import or replace the package | `tenant.parables.manage`, `tenant.parables.labs.packages.view`, and `tenant.parables.labs.packages.modify` |
| Turn Lab mode on or off | `tenant.parables.labs.view` and `tenant.parables.labs.modify` |

The Workspace Administrator role holds all of them. A reader without the
file permission sees the app alone. The browser still downloads every file of
the package to run the app, so keep private data out of the package and read
it through declared Plots.

## What a Lab does not do

* A Lab draws no structured Pieces or Preferences. Keep filters and
  navigation in React state, inside the app.
* A Lab does not store query results as Reports. Results show in the app.
* Deploying a Lab does not copy data. Each Workspace that receives it needs
  its own Provider connections and Artifacts.
* A Lab with content cannot become a structured Parable. Start from an empty
  Parable for that.

[Lab Parables](https://docs.parable.work/ponder/powers/lab-parables) explains
how Ponder builds and changes a Lab for you.


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