Skip to main content

Sources

Docs: Panes, Preferences, Policies, and Execution context. Web App: Select a Pane, open Plots, and choose a named side tab. Results, History and Details appear below its SQL, and Read by lists the Pieces that read the Plot. Under the Pane’s own Plots, Its Panes lists the Plots of the Panes it declares, to read; a Page’s Plots tab lists those alone. React source is in Programming. Adding a Plot to a Pane opens a dialog that asks for its name and one sentence on what it computes. The name seeds the Plot’s handle and its catalog address; the address stays fixed after the Plot is created, even when it is renamed. Every Plot needs a description, shown in the band under its header. While you edit SQL, an advisory check runs after a short pause. Warnings & Errors are underlined in the source, marked in the gutter, and listed in the footer under the SQL, with the other checks. Hovering one shows its message and code; one with a suggested replacement shows it as a button. The footer also lists findings without a source location, such as a Plot that blocks publication. Choosing a finding with a location moves the editor to it. A Plot’s recorded warnings and errors hold a Publish, as every Piece’s do. The panel distinguishes Advisory SQL from Server validation. A pending or unavailable check is not an all-clear. Missing catalog access or an unavailable browser validator leaves the source editable and keeps the existing save controls available. Server checks and Policies still apply when you save. Saving definition SQL records server validation diagnostics. The server also plans the SQL the way a read does, without running it: SQL the query layer cannot plan records PV_STMT_004 with the reason, so a Plot that would fail on every read says so when you save. An aggregate in ORDER BY reads table columns, not output aliases: ORDER BY MIN(priority) where priority is only an alias records PV_REF_003 or PV_STMT_004. The save plans SQL that takes parameters with the Plot’s saved parameter values, and SQL that reads another Plot against that Plot’s last output schema, so this holds in Personal mode, which has no Prepare. Saving new parameter values alone checks the SQL again. It records no finding for a parameter that has no saved value, or for a read of a Plot that has no output schema yet. When the SQL reads another Plot, a column the query layer cannot find there is not a finding: that Plot’s last output schema can trail its SQL. With Published Versions, Prepare and Publish also plan every Plot with its parameter defaults and the other Plots bound, and SQL the query layer cannot plan is a plot_plan_failed finding that blocks publication. An unavailable validator does not prevent saving: the draft, last successful schema, reference pins, and dependencies stay available. Each draft keeps its own dependencies. Completed, trustworthy reference checks replace that draft’s dependencies; references that would introduce a cycle retain the previous dependency set. Publishing carries the SQL’s recorded check into the Version, including when it replaces an existing Plot. For older Versions that lost that record, Warnings & Errors uses the checked Proposal’s result for the same Plot and exact SQL on that deployment. This read changes no published content. If no matching check exists, it says the check is unavailable and asks for a re-check; it does not remain pending when nothing is running. A Plot that reads a sibling Plot is checked again once that sibling’s columns change in the same save, so a save that adds a chain of Plots (C reading B reading A) derives every link: each pass settles one more level. A save shares one ten-second budget across all Plot validation calls, including those later passes. When time runs out, the remaining checks are skipped and those Plots record PV_SYS_001 (unchecked). Your SQL draft still saves, but unchecked Plots cannot be published. This bounds validation waiting; database work can take additional time. Saving an unchecked Plot again checks it again, even when its SQL did not change, so an agent that cannot reach the re-check below recovers by sending the Plot once more. A save that does not send an unchecked Plot leaves it unchecked. An explicit re-check on the Primary or Proposal retries unchecked or stale verdicts with the same shared budget, and verdicts that waited on a Plot the SQL reads (PV_REF_007), since that Plot may have a schema now. It skips current completed verdicts, including SQL errors that need an author edit. Re-checking does not change your authored content, but it writes check results onto the Plots it checks, so a Proposal re-check runs only on a Pending set. A Proposed or published set is sealed, and its re-check is refused with PA-AU-004; Prepare checks a Proposed set’s Plots without changing them. See the Primary re-check API and Proposal re-check API. An archive import runs the same checks before answering. Imports and explicit re-checks also plan Plots with saved parameters and retry Plots that were left unchecked or waiting on another Plot’s columns. They continue while each pass records a new schema or leaves fewer Plots waiting, and stop when a pass makes no progress. A persistent SQL error or an unavailable planner still needs an author edit or a later re-check; importing does not trust schemas from archive bytes. The web app allows up to five minutes for an import or re-check and names the command if its request times out. An import that was already saved keeps its content even if its later checks could not finish. An imported Pipeline may have checked columns before it has a planned or stored output. Planning a Plot that depends on it can use those checked columns, with each column treated as nullable because the check does not prove non-null values. This provides types for planning only. Reading that Pipeline still requires the planned output and data that its normal read path requires. Static validation derives schemas for SQL without parameter defaults. A save of SQL with explicit defaults, including [], plans it right after the save with those defaults, as publication does, and records that schema beside the SQL, so Plots that read it in the same Changes bind to it at once. Planning runs no SQL, so a Plot that calls a model costs no call. A Plot the planner refuses keeps its last successful bound schema. Publishing checks the candidate definition and its parameters, diagnostics, and scheduled plan before sealing it. A Plot whose SQL takes $ inputs needs no saved defaults to publish: publishing plans it with the inputs unbound, as long as no output column takes its type from one (cast the input when one does). A Report member still needs its values, because its run binds what the definition saved. When publication is blocked, the current Prepare findings identify the Plot and retain the server’s reason, including after reloading the Proposal. A Publish refused by the gate names the same findings in its own message. A Plot returns exactly the columns its Pane’s Perception declares for it. A column the declaration does not have, a declared column the SQL leaves out, or a column of another type (text where a number is declared, say) is an error under Warnings & Errors (PA-AU-047), judged from the schema recorded for the saved SQL. Until planning has typed that SQL, the schema is the save’s check’s, which names a shape it cannot type, such as a list of structs, as text; a list or object column it named as text waits for planning. Prepare and Publish refuse such a Plot, naming each column, because every read of it would be refused. End a Plot’s SQL in an explicit column list, never SELECT *. A value that breaks the declaration when the Plot is read, such as a number past the declared maximum or text its scalar does not accept, refuses that read with PA-AU-047. The refusal names the Plot, never the value, and the Pane shows it as the read’s error. A Plot whose SQL reads such a Plot gets the same refusal, naming the Plot whose rows broke it. A Plot needs a contract: its Pane’s Perception declares it under plots, or declares preferences so its rows come from the Pane’s kind. A Plot with neither has no contract, and every read of it is refused (“Plot requires an authored Perception computation contract”). Prepare and Publish refuse it, naming the Plot and its Pane. It never stops another Plot from reading: SQL that names it is refused as not ready, and every other read goes on. A Pane whose Perception declares preferences shows its primitive’s type, and a Plot it owns that its Perception does not declare returns rows of that type: any of the type’s fields, as columns of the same names and types. Its rows are the type picked to the columns its saved SQL returns, and it takes no input. A column the type does not have, or one of another type, is PA-AU-047, named against the type (“it returns quarter, which RegionSales does not have”). Add the field to the type or leave the column out. Until the save records the SQL’s columns, the Plot’s rows are the whole type. Return a Temporal.Date field as a DATE column and a Temporal.DateTime field as a TIMESTAMP, with a CAST where the source holds the day or time as text (Perceptions). To clear a Plot finding, return the Changes to Pending with Patch Proposal, fix the SQL, propose them again, and choose Prepare Patches in the Publishing options. Prepare runs every check on the current Changes, the Plot checks included. A finding the fix resolved stops blocking, and Publish Patches becomes available. Saved findings belong to the exact SQL and selected Workspace and definition. They replace advisory feedback for that saved text and clear when a completed server verdict clears them. Publication findings show the server’s reason for the named Plot; changing its SQL starts a new check. The catalog follows the selected Workspace and Changes. Historical versions remain read-only and do not borrow the current definition’s advisory catalog. Unsaved definition siblings can contribute trustworthy schemas to the current check. Typing a Plot back to its saved SQL withdraws its unsaved draft, including when an earlier save is still pending. Other Plots then validate against the saved schema again. In the Admin editor, select the Workspace whose data the Plot uses before checking it. This editor uses the Workspace adapter and its tenant.data.catalog permission, alongside current RACI and Policies. Without catalog permission, you can keep editing and saving. Unavailable checking is shown as unavailable, never as a successful check against an empty catalog. The native Admin catalog operation separately requires admin.tenant.data.catalog. Native Admin Prepare and Publish use the selected Workspace and authenticated staff account. Planning and execution still require their current Workspace authority. A successful check in one Workspace does not certify the data or Policies of other deployment targets. The SQL validation specification describes diagnostics, catalog snapshots, and publication checks. For typed access from TypeScript Pieces, the data SDK specification explains manifests, reading selection, and pagination. It distinguishes the implemented runtime and server foundations from execution-host integration.

Structure

A Plot is a typed computation, and it always lives on a Pane. The Pane’s Perception declares PlotContract<Input, Output> under a local Plot name, or, when the Pane declares preferences, the Plot returns rows of the Pane’s primitive type under its handle. SQL is the current implementation language; React rendering belongs to the Pane. The SQL is the Plot’s implementation: it is saved with the Parable’s draft and versioned with it. A Plot has no Policies of its own; its Pane’s Policies govern it.
The name matches the Plot’s handle when first saved, and the Plot must belong to the Pane. Until the Perception declares it, the Pane’s code has no plots.activityFacts and Preview reports that the Plot’s contract is unavailable. Perceptions lists the type to declare for each kind of SQL column. Other Pieces read a Plot by address. A Page or Pane that declares the Plot’s Pane in its Perception reaches the Plot in code as panes['<pane>'].plots['<plot>']() or panes['<pane>'].plots.run('<plot>'), and those code references are what Read by lists on the Plots tab. Deleting that Plot, or its Pane, is refused, and the refusal names the readers. Deleting a Pane that another Perception declares is refused the same way. A Plot’s SQL reads a Pane’s Preferences by name: parables.<parable>.<pane>, one row with a column for each field of the Pane’s preferences. <pane> is the snake_case of the Pane’s handle, the rule a new Plot’s slug follows: the Pane timeSpendScope is time_spend_scope, and readerAIUsage is reader_ai_usage. Handles are unique ignoring case, so no two Panes share a name. A Plot whose slug is a Pane’s name keeps it, and that Pane cannot be read by name until one of them is renamed. Preferences that may be null (Choice | null) have the same columns, all null when nothing is set. The row is the value the Pane’s code holds on the reader’s screen (preferences.current(value)), else the reader’s own setting saved under the handle preferences, else This Parable’s under that handle. A setting saved under any other handle is not read, so with none the row is all null; the Plot supplies its default with COALESCE. The SQL editor’s catalog lists each Pane it can read this way. A Pane is never a Plot dependency: changing its values reads the Plot again (PARABLE-5675). A Report run does not read Pane values yet: Prepare refuses a Report Plot whose SQL names a Pane (PARABLE-5877). Panes that read one Plot with the same parameters, the same Pane values, the same reading and the same View as while a read of it is still running share that read: the browser sends one request and each Pane gets the same rows. Once a read answers, the browser keeps its result for the session. The next read of that Plot with the same parameters, filters included, the same Pane values, the same reading and the same View as answers from it at once, so moving between Pages or tabs never waits for data; a Pane’s own loading transitions still play. Kept results are dropped when they could be out of date:
  • When Latest moves to a newer Refresh while the Pages read Latest, every result read as Latest is dropped. A result read from a chosen Refresh stays.
  • When a read or a write is refused, every kept result is dropped, so no row read under the reader’s old access answers again.
  • When the reader saves or deletes a setting, the Parable’s kept results are dropped, since a Plot’s SQL may read settings.
A new Version, a saved draft or another View as is another reading, so its reads go to the server. An author’s SQL preview is never kept. The kept results are bounded by size, and the least recently used go first. A Pane that needs newer rows than a kept result calls its hook’s breakCache(): that read goes to the server and its answer replaces the kept result. rerun() runs the hook again and is answered from the kept result when there is one. Another Plot whose SQL reads this Plot by its catalog address is not listed yet, and deleting a Plot that only SQL reads is not refused.

Plots read as views

A Plot nothing stores is read as a view of its SQL. A stored Plot is a Pipeline (its Pipeline switch, requiresReportingRun, is on), has a schedule, or asks for materialization; every other Plot whose SQL binds no parameters is a view. SQL that names parables.{parable}.{plot} for a view, in another Plot, a Pane’s query or a Report run, runs the view’s SQL as part of its own, so small Plots compose the way SQL views do: a Concept Plot can union one mapping Plot per Provider, each of which can read further views. Views compose; they do not make reads fast. A view’s SQL runs inside every read that names it, once for each place it is named, so a Concept that unions many mapping Plots is computed again for every Pane that reads it. Parables are product experiences, and a Page must open fast: make any expensive query a Pipeline, judged by what it scans, joins, aggregates or calls, and let other Plots and Panes read its stored output. Work down from what each Page shows first. Readers see how old stored data is and can refresh it; see Reports. To measure instead of judging, record in the editor drawer’s Performance tab and open the Page a few times: each Plot read shows its time, rows and size, whether it was live or stored, and where its server time went. A live read that holds the Page back on every open is a Pipeline, and the tab’s hints say so. Ponder reads the same recording while you record.
  • Columns. A view serves its output schema, the columns a stored table of the Plot would serve: the schema publication planned for its SQL, or, for SQL a draft added or changed, the schema the draft’s last save derived, with every column nullable. If its SQL no longer returns those columns with those types, the read refuses until the Plot is planned again, or for a draft, until a save derives its schema again. A value of the declared type in another storage is served as declared: text that SQL functions return as Utf8View or LargeUtf8 (CAST(… AS VARCHAR), string_agg) reads as Utf8, and a timestamp in another unit in the declared unit. A finer timestamp, such as the nanoseconds CAST(x AS TIMESTAMP) returns, is rounded down, so an instant never moves into the next second or day; inside a list or struct it refuses the read.
  • Version. A Pane read and a Plot preview read each view from the definition they show: a draft reads the draft’s SQL, so an author previewing a Concept Plot in Changes sees their draft mapping Plots, including Plots only the draft has, and a published Version reads that Version’s SQL. SQL outside a Parable reads the current published Version.
  • Views of views. A view can read another view. Views that read each other are refused: reading one answers Plot views form a cycle: … reads …, and publication names the cycle instead of planning it.
  • Report runs. A Pipeline may read views of its own Parable. Planning writes each view’s SQL into the run’s plan, so the run’s pinned inputs are the Provider tables and stored Plots under the views. A view of the run’s Parable must come from the Version the run captured; a run captured before a later publication of the view is refused, and running the Report again uses the new Version. A run whose view the Workspace catalog serves from an earlier Version is refused with that cause instead: the catalog has not caught up with the captured Version yet, or that Version was retracted. A run a few minutes later reads the Version the Workspace serves.
  • Schedules. A Plot on its own schedule plans with no principal, so it cannot apply a view’s Pane Policies and is refused. Read the view from a Pipeline, or store it.
A Plot that someone makes while using an app belongs to that person or Workspace, not to the app’s definition. There are no screens for making one yet. The input describes positional parameters and the output describes rows. A fixed tuple has exact arity. A dynamic query builder can declare an array of supported scalar types. Both paths validate parameter values and the SQL’s placeholder count. The contract exists even when no SQL has been saved. React imports other application components through declared Pane references. It accesses owned logic through its Pane SDK. For a Plot declared with no parameters:
  • plots.activityFacts() is a hook exposing status, rows, refreshing, rerun and breakCache. Pane code draws three states: reading (status is running with no rows yet), empty (ready with no rows) and rows. A read that fails never reaches it: the host shows the failure in the Pane’s place, with Retry (below).
  • plotApi.run('activityFacts') runs from an action handler.
  • result.meta identifies the data that answered the request, including a resolved Report when relevant.
A Pane can also call the Plots of a Pane it declares, through that Pane’s component (PaneComponent.plots), without rendering it. status is running while a read with no rows on screen is in flight, the first one included, then ready. rows is empty until the first answer arrives, so empty rows mean no data only when status is ready. Render the states in this order, with no error branch:
A shared-setting or seat change in another session reaches open Pages through its live connection. Their Panes resolve again and their Plot hooks read under the current values and access. A save or publish brings a draft’s new revision through the refreshed context; it does not reread an unchanged published Version. Ponder messages and run activity do not invalidate Plot results. A hook already showing an access refusal keeps that error until focus returns or the Pane calls rerun. A failed read’s error has a message and a code. The code is the server’s error code when it supplies one, such as QL-TO-002; a bridge code such as TIMEOUT when the host does not answer; or PLOT_FAILED when the failure has no code of its own. The host’s error for a hook shows the message, and run, preview and readReport reject with the error, for the action that called them to handle. A read a filter Policy refuses has the code QL-PL-015, and error.details.filter names the filter: the Plot does not output a column the filter reads, so none of its rows can be shown to this reader. That is not a failure to retry. For a hook the host shows it as hidden (below); an action that gets it from run shows it the same way, such as “Hidden by” and the filter’s name. The hook reads again when a Preference changes, when the reader, the Workspace, the definition, or the data reading changes, when the window regains focus, and on rerun() and breakCache(). A kept result answers it when there is one, except for breakCache(). When it reads the same Plot again, it keeps the rows it shows on screen until the new ones arrive: status stays ready and refreshing is true meanwhile, so a Pane never blanks while it reads again. A Pane can use refreshing to show that newer rows are coming. A first read, new parameters, a read after a failure, or a read after the reader’s access changed starts with no rows and status running, so no row read under the old access stays on screen. A Pane that shows its empty state without checking status reports no data during those reads. The hook waits 70 seconds for a read. The application ends a Plot data request at 55 seconds and the read then fails with that error, so a slow read stays running until its rows or its failure arrive. A first read with no answer after 70 seconds fails with the code TIMEOUT. A read again with no answer keeps the rows it left on screen, still ready, and refreshing returns to false. A Pane resolution waits up to 60 seconds because it can queue behind the Page’s Plot reads. A setting’s save or delete waits up to 65 seconds, because a Versioned setting’s save publishes a Version or proposes Changes before it answers. One that outlasts the web app’s 60 seconds rejects with PREFERENCE_WRITE_PENDING: it is still running on the server (Preferences). Other host calls wait 10 seconds, except query.run (70 seconds) and chunk loading (60 seconds). A Plot hook retries a short server failure (HTTP 500, 502 or 503) twice: after one second, then after two seconds. It stays running during those pauses. When a read fails for good, the host replaces the Pane with an error that names the Plot, shows the failure’s message, and offers Retry. Retry reloads only that Pane and reads its Plots from the server, not from kept results; the other Panes keep their state. A Preference change, a new context such as the window regaining focus, or a change to the Pane’s input also reads the failed Pane again. Leaving a Pane cancels its scheduled retries. Refusals, invalid requests and expired time limits are not retried: the Pane shows their error at once. A read a filter Policy refuses (QL-PL-015) is not a failure: the Pane shows “Hidden by” and the filter’s name, says the data can’t be narrowed to the rows the reader may see, and offers no Retry. It reads again when the context changes, such as the reading or View as. A preview that draws Panes without running them, such as the picture Ponder takes of a Page, reads no Plot. A Pane that reads one shows a calm “Live data isn’t drawn in previews” in the picture, not “Could not read” with Retry. A Plot that a chosen older Refresh did not capture (PD-RESULT-001, or PD-RESULT-002 for a Plot it reads) is not a failure: the Pane shows Not in this Refresh with Read Latest, which moves the reading to Latest, and no Retry. A Plot that the chosen Refresh’s Version computes as a view, rather than storing it, reads under that Refresh as its view over what the Refresh stored, with the settings and directories it captured. Only a view that needs other data, such as the Pool’s current schema, answers Not in this Refresh (PARABLE-5944). On Latest, a reader is served the newest Version whose stored Plots can be served (see Reports). Its readers stay on the previous Version while a new Version’s first Report runs, so they never wait on a gap. A reader who opens the new Version early, such as its publisher, reads a stored Plot with no data yet (PD-RESULT-002) as waiting: the hook stays loading, so the Pane draws its own loading state, never the relation name, and draws its rows when the Refresh completes. A Pane that runs its Plot itself (plotApi.run) waits for the Refresh the same way. Other not-ready reads on Latest draw Refreshing… over the kit’s skeleton. A Pane may catch a Plot hook’s failure in its own error boundary and keep the rest of itself drawn, as Team Time Spend’s Home does when its teams can’t be read. The host still knows a read below failed: when the reading, a Preference or the context changes, it draws that Pane afresh, so its own error state does not outlive the reading that caused it. A slow read ends with the code QL-TO-002 and a message that names the limit that ended it, which the host’s error shows. Which limit that is depends on where the read runs. On a local stack the Query Layer gives a Plot read’s query 40 seconds, then stops it: “This query took longer than 40 s, the limit for a Plot read, and was stopped.” Staging and production give a query 3600 seconds, so the web app’s server ends the read at 55 seconds and cancels it, which stops the query: “This Plot read took longer than 55 s, the web app’s limit, and was stopped.” Ponder stops waiting for a Plot read it runs as a tool after 30 seconds. When too many queries are running, the Query Layer does not start the read. The read fails with the code QL-IN-008 and the message “Too many queries are running right now. Try again in a moment.” The host shows it in the Pane’s place, and Retry reads it again. Generated types come from the Perception’s declared inputs and outputs. A Plot whose rows come from its Pane’s primitive type is typed as rows of Pick<Type, columns> from the columns its runtime binding carries. SQL planning and execution check that contract, including columns, nullability, and scalar representations. The editor’s Preview sends its edited SQL under the selected Plot’s same contract and its Pane’s current Policies, so it works only for someone who may save that SQL (Security). A missing saved implementation is reported without creating SQL or data. SQL Output contracts support explicit object fields and homogeneous arrays, including nested objects and arrays of objects. Dictionary maps, tuple columns, multidimensional arrays, and mixed unions refuse at compilation. These SQL restrictions do not limit ordinary Perception primitives used by Panes and their settings. Saved planning accepts equivalent Arrow string storage and timestamp units while preserving the original values and timestamp precision. Mounted Plot actions and hooks retain the contract they were compiled against, including hooks reached through a referenced Pane. If the selected contract changes, existing callers refuse until refreshed; they cannot silently inherit the new input or output type. Changing current access still takes effect on each request.

Services

Kysely is available by default. It compiles query structure into SQL and a separate parameters array. Preview the result through:
Previewing runs the compiled query under the declared Plot’s contract and its Pane’s Policies, and saves nothing. A Pane can call preview in the Piece editor’s canvas and on a Page in Ponder’s chat. Both send the SQL to the server, which runs it only for someone who may save it, at their own seat whatever their participation mode, and refuses anyone else with HTTP 403, PA-AU-019 and the reason before any query starts (Security). A refused preview is a refused read, so it drops the kept results and the Page’s Panes read again: offer a preview only where the Pane’s Policies allow edit.plots or create.plots. The preview camera and Labs refuse it. run, the hooks and readReport send no SQL and work for every reader of the Plot. SQL is saved only as a Plot’s implementation: through the authoring save, or, when the Pane’s Perception declares preferences: Versioned<PlotSql>, as the Pane’s setting under the Plot’s handle. That save is the Plot’s new SQL in a new Version: it passes the Plot gate like any edit, needs edit.plots, and each Version keeps the SQL it was published with (Preferences). A query built in the app any other way is not stored. A Pane that shows SQL rather than running it, such as a mapping table or a drill-down, reads a Plot’s saved SQL with plotApi.source('meetings'), or a declared Pane’s with panes.meetingZoom.plotSource('zoomMeetings'). Either returns the SQL of the definition being viewed: the Version, or the Changes open in the editor. It returns null when the reader’s Policies do not let them view the Plot’s SQL (view.plots); the Plot still runs for them. A surface that holds no Parable package, such as a Pane preview in chat, refuses the call. SQL definition source can also retain parameter defaults alongside its code. Those defaults belong to the definition, survive publication and archive export/import, and must match the Perception’s declared Input. Older published versions retain their own defaults after a later edit changes or clears them. React and CSS sources cannot carry SQL parameters. An explicit empty array stays distinct from omitted defaults. Use a declared tuple or scalar array for nonempty parameters. Native Dates serialize as timestamps; strings, finite numbers, booleans, and null retain their values. Parameters remain bound values and are not interpolated into SQL. Omitting a live-query override uses saved parameters; an explicit array replaces them, including an empty array. Use explicit casts when needed to keep the planned output type stable. Native numeric values must be finite. Integer-valued numbers must fit JavaScript’s safe range, from -9007199254740991 through 9007199254740991. This also applies inside nested values, parameters, saved defaults, and Report results. Unsupported values refuse before a write or result completes; retained values and Report bytes are never rounded or rewritten during a typed read. Finite fractions remain supported. Use an explicitly declared string contract when an application needs to retain larger integral identifiers as text. A Plot edit uses the revision last read. A refused save preserves the local draft; reload explicitly before replacing it. plotApi.key('activityFacts') returns the declared Plot’s stable identity; the identity grants no access by itself.

Large results

A Pane’s Plot read returns at most 10,000 rows. Read only what the Pane draws, in this order of preference: aggregate in SQL, store heavy work, then page through rows a person is browsing. The examples use Team Time Spend’s action_hours (one row per person, Action and day) and time_spend_facts (hours per person, category and day), both stored by Pipelines. 1. Read what you draw. Aggregate in SQL to the grain the Pane shows, and never send raw rows to the browser to add up there. Apply filters, the scope and corrections before the GROUP BY. A Home chart of hours per category reads a few dozen rows, not every fact:
Totals come from an aggregate, never from adding up pages. A reader’s filter Policies remove rows before COUNT(*) or SUM sees them, so the total is exact for every reader and under View as:
2. Store heavy work. Make an expensive query a Pipeline and read its stored output (see Plots read as views and Reports). time_spend_facts is a Pipeline: the categorizing and corrections run once per Report run, and every Pane reads the result. 3. Page through rows only for a person browsing them, such as a table, a list or a drill-in, and never to assemble a total. Every page is a full Plot read: the server plans it, binds its inputs, runs it and sorts the whole result again. Prefer keyset pages over a stored output. The Pane passes the last row it shows as parameters and asks for the rows after it, ordered by columns that end in a unique key:
The Plot declares those parameters in its contract, and the Pane reads the next page when the person asks for it, for example from a Show more button that calls the hook with the last row’s values. A result the session kept answers a page read again at once. A value several Panes share, such as a chosen person, can come from a Pane the SQL reads by name instead of a parameter. OFFSET is for shallow pages only: the server still sorts every row and skips the ones before the page, so a deeper page costs more.
Pages follow the Page’s reading. When the reader has chosen a Refresh, every page reads it. When the Page reads Latest, nothing keeps a later page on the Refresh an earlier page read: if a new Refresh lands between them, rows can move between pages. A read of a stored Plot names the Refresh that answered in its result’s meta.reading.reportRunId, but a paging Plot like the one above reads stored Plots by name and its result does not say which Refresh they came from. Pinning pages to one Refresh is not available yet. 4. The 10,000-row limit is a guard, not a target. When a Plot has more rows, the Pane gets the first 10,000 and its hook’s truncated is true. The host shows no notice of its own, so a Pane that could be cut off says so, and the editor drawer’s Performance tab flags the read (“Stopped at the 10,000-row page, so its Pane drew part of the answer”). A Pane that is cut off is a bug: aggregate, store or page instead. 5. Ponder and other agents follow the same rules. Ask the aggregate question in SQL instead of paging through rows to count or add them up. The Plot read operation an agent calls takes up to 10,000 rows per page and answers with nextCursor while more rows remain; each page is again a full read.

Storage

A definition version identifies code and schema. A Report identifies produced data. For a Pipeline, Latest (the API’s Current) selects the newest completed Report whose output fits the selected definition. It can come from an earlier compatible definition. Publishing does not run a Report or change its schedule. A Report run on query-runner stores every Plot’s output as a table of its own, written once and never overwritten. A later run cannot change an earlier one, so choosing an older Report returns the rows it held when it completed, in the order the run wrote them. A Report run that fails or is cancelled stores no readable output, even for Plots that finished. SQL that names parables.{parable}.{plot} for a Pipeline reads the output of the newest completed Report that fits the published definition, and an empty table until one completes. A completed run’s rows show on the next read. For a Plot nothing stores, the same address runs the Plot’s SQL (Plots read as views). That output was produced under its run’s principal, so it is served only to a reader who still has that run’s access to its inputs (Security). A run that completes reaches parables.{parable}.{plot} within moments: completing it tells every Query Layer server to drop the catalog it cached for the Workspace, and the next read loads the new one. When that notice fails, it is sent again. A server that has not heard it keeps answering with the output it cached, or the empty table, until its catalog expires three minutes after loading and a reload finishes. Each Report keeps its newest 30 completed runs and its recent manual Refreshes (Reports). An older run leaves the Refresh menu, Latest and parables.{parable}.{plot} before query-runner deletes its tables, so no read ever sees a table that is partly deleted. Choosing a Refresh pins that run; an unavailable or incompatible result does not fall back to Latest. Ordinary SQL Plots and previews can query completed pipeline outputs while using their own SQL and parameters. Direct reads of a materialization retain its captured inputs and refuse different overrides. Use plotApi.readReport('activityFacts') to read completed output with its captured inputs. It returns the Perception’s typed rows without requiring today’s input tuple. It uses the selected Refresh, or Latest’s newest compatible completed result, and refuses when no materialized result is available. run and preview keep their declared input requirements. A named read runs the Plot’s SQL from its implementation in the selected ref or sealed Version. The result names that implementation row and its version (sourceImplementationId and sourceImplementationVersion), and a Report records the same pair for each Plot it ran. A materialized response retains its producer contract and source metadata; consumerContractHash records admission for the selected definition, and materialized distinguishes captured output from an ordinary query running in Report context. SQL catalog addresses use the stored Parable and Plot slugs. Both segments begin with a lowercase letter and contain lowercase letters, digits, and underscores between groups. Hyphens and case-only replacements are refused. A new Plot’s slug is the snake_case of its handle (revenueForecast is revenue_forecast) unless its author sets one, the same rule a Pane’s name follows. Renaming a handle does not move those addresses. A Plot deleted on a ref frees its address on that ref, as a deleted Piece frees its handle, so a Plot that names that slug, as an upload from another Parable does, can take it there; a new Plot that names none never takes a deleted Plot’s address. Query builders take the stored slugs from the selected definition’s metadata; they do not derive an address from a handle. Publication plans Plot dependencies from the candidate definition’s schemas. A missing dependency or a cycle refuses publication. The cycle check covers the Workspace’s published Plots as their dependencies will stand once the Version is published: two Plots in one Version that read each other are refused, and a Version that turns a dependency around (A read B; now B reads A) is not. Planning does not fabricate rows or require an existing Report. Results retain the producing source and Report separately from the consuming definition. Publishing plans each Plot before it seals the Version. The Version carries every Plot’s published SQL and planned output schema, its dependencies are recorded against that Version, and the published-Plot catalog returns the SQL and schema from it. Publishing leaves a Plot that already carries its plan as it is, so planning adds no change to a Plot the publish did not edit. Publish and Prepare plan every Plot before they answer, which takes minutes on a large Parable: a Team Time Spend copy took 75 to 306 seconds on a local stack (PARABLE-5285). The web app gives either the 60 seconds it gives every API call. When that limit ends a command, the web app cancels its request and answers 504. A publish stopped while it plans has written nothing. A publish or Prepare that Ponder runs as a tool has its own limit: web-api ends a Ponder tool’s operation after 90 seconds for a Message’s call and 2 minutes for a Run’s (PARABLE-5358). A command stopped there answers 504 and says it ran past the time limit of the call that sent it, because sending it again through Ponder would stop at the same limit. A command whose request was cancelled some other way answers that sending it again with the same idempotencyKey returns its result if it finished and runs it once if it did not. When a Plot cannot be planned for a reason other than its SQL, such as an unavailable Query Layer, the build failure (PA-AU-015) names the Plot and the reason: “Plot teamHours could not be planned: the Query Layer is unavailable.” An internal Query Layer failure reads “the Query Layer reported an internal error”, and its own message goes to the log, because it can carry storage paths and addresses. Plots that share the same SQL are all named. A Plot whose SQL the planner refuses is named the same way, with the planner’s sentence, in the build, in the publish gate and in Prepare. In Prepare’s gate an unavailable or failing Query Layer is a finding on each Plot instead. A run of a Plot stores its output with the column types of the Plot’s plan. A UNION column keeps a scalar type such as Identity.Name only when every branch has it: when one Provider’s titles are names and another’s are plain text, the merged title is text. A Pipeline’s table, parables.{parable}.{plot}, is the newest completed run whose output fits the Plot’s contract and plan, the same output a Latest read selects, so a Plot that reads it plans against the columns its producer planned. A run changes neither the Plot nor its Version. The table’s freshness watermark is the pin time of the Workspace’s last successful run of that Plot, kept per Workspace, so a Workspace that received the Plot in a Delivery has its own and leaves the origin’s alone.

Its Panes’ Plots

A Page’s or Pane’s Plots tab also lists the Plots of the Panes it declares, under Its Panes: each such Pane once, as a closed group with the number of Plots it owns, read only when it opens. Choosing one shows its SQL read-only, the reader’s draft included, with its Pane, its name and Read by. Open goes to that Plot in its own Pane’s Plots tab, where it changes. A Page owns no Plots, so its Plots tab lists its Panes’ Plots alone.

Run history and status

A Plot’s editor has three tabs below its SQL. The Refresh control at the top right of the editor, Results and History share one selected reading, on /ponder and /admin/ponder alike.
  • Results shows the selected reading. On Latest it holds Preview’s rows with their count and time. Above them it lists what Prepare would block this Plot for, if anything: Preview runs Prepare’s own checks on the edited SQL, judged by how the Plot runs (a Pane, a schedule, or a Report), and still shows the rows. Preview in the Refresh menu shows the same for each reporting Plot. On a stored Report run it holds that run’s own rows, read without running the Plot. A run that is queued, running, paused at its budget, failed, cancelled or skipped shows that status instead of rows, and a failed run shows its message. A Plot that finished inside a run that is still moving, or that did not complete, has no readable rows either: only a complete run is readable. When the selected run does not include the Plot, because the Plot is not a Pipeline or the run has no result for it, Results says so and reads nothing. Preview runs on Latest only, so it is unavailable while a stored run is selected.
  • History lists every stored Report run, newest first. Each row is named by when the run happened, in your time zone and clock: 12:31pm ET today, Sep 29, 9:00am ET before, with the whole moment on hover. Results names a stored run the same way, as in Refresh Sep 29, 9:00am ET · 1,204 rows. Each row shows this Plot’s status, whether the run was Manual or Scheduled and for which Report, who pressed Run, when the Plot started and last changed, how long it ran, how long it waited to start, its row count, its spend in US dollars and paid calls against the caps captured when the run started, and the catalog address with the run’s short id. A row count the reader may not see reads Hidden, never 0. Choosing a row selects that Refresh in the Refresh menu and the address (refresh); choosing one in the menu selects its row.
  • Details lists the catalog address parables.{parable}.{plot}, whether Report runs include the Plot, its published caps and its output columns.
The same runs in History, read in New York (en-US) and in London (en-GB) after midnight there: A History row for a run that is still moving offers Cancel Refresh, and a row where this Plot is paused at its budget offers Raise budget. Raise budget opens a dialog with the Plot’s spend against its caps and takes new cost, call and runtime caps for that run only. A control the reader may not use stays visible, disabled, with the reason. See Cancel a run or raise a paused Plot’s budget. The Refresh trigger marks the newest run while it is queued, running, paused at its budget, failed or cancelled, and while that run is moving the menu offers Cancel Refresh. Opening the menu reads the runs again. While a run is still moving and the page is visible, the editor reads the runs again every 5 seconds, then every 30 seconds once the newest moving run is five minutes old, and stops when every run has ended. A hidden page pauses and catches up on its next read after it is shown again.

Security

A Plot is governed by its Pane’s Policies for the same Version. Source inspection uses view.plots; source edits use edit.plots. The rows follow view.preferences, and the filter Policies that cover it narrow them. Permission to read data need not disclose its query. The published-Plot catalog returns each Plot’s SQL. A person sees a Plot there only with an active Workspace membership, a seat, output See and source See. The Query Layer and query-runner read the catalog with a service token, which web-api serves as the System user. The System user holds no membership or seat, so its read skips those person checks. It still returns only the Plots published to the Workspace named by the request’s X-Tenant header. Using a React renderer requires permitted Pane/value access. It can be delivered without granting its source-editor view, but executable JavaScript necessarily reaches the browser. CSS has the same limitation. These restrictions cannot promise secrecy for delivered code or styles. A Plot read may carry SQL of its own, a constructed preview, which runs in place of the Plot’s saved SQL. The server runs it only for someone who could save that SQL, by the three checks a save makes: the Parable edit permission (tenant.parables.manage, or admin.parables.manage for staff), a seat that edits Pieces (Publishing or Primary), and edit.plots or create.plots from the Plot’s Pane Policies. Anyone else, a Viewer included, is refused with HTTP 403 and PA-AU-019 before any query starts, whether the request comes from the web app, the API or Ponder. The reason says which check failed: that only people who can edit the Parable’s Plots can run their own SQL on them, or that it needs edit.plots or create.plots on the Plot’s Pane. Policies cannot admit a seat that edits no Pieces: a grant that names a Peeping or Pinging person, or a Role they hold, shows them edit.plots in the Pane’s capabilities, but the server still refuses their SQL. The checks use the reader’s own seat, as every Plot read does, so a participation mode below it changes nothing. A Pane’s preview reaches them from the Piece editor’s canvas and from a Page in Ponder’s chat, and the Pane gets the server’s code and reason; the preview camera and Labs refuse it without asking the server. One frame renders a whole Page, so the host cannot tell which Pane’s code sent a request: a preview can name any Pane’s Plot in the Parable, as run can, and these checks bound what it runs. Under View as, the seat and grants of the person being previewed decide, and the permission is the viewer’s own. A read without SQL runs the saved SQL and needs only view.preferences. SQL you run inside a Parable without saving it, from a Lab Page or with Ponder’s SQL tool, works for anyone who can read the Parable: a seat on it and a role that reads Parables. It reads that Parable’s Plots by their catalog addresses, parables.<parable>.<plot>, each under its Pane’s Policies, so a reader gets the rows the Panes would show them and nothing more, and a Pane whose Policies refuse the reader refuses the SQL. generate_series and range work there too. A Plot read as a view still reads its own sources. A Plot that takes inputs or runs as a Pipeline is read from a stored Refresh that matches its current output. With none, or when your Changes edited its SQL, that Plot has no data there yet: the SQL, and a Pane whose Plot reads it, is refused with HTTP 409 and PD-RESULT-002, which names the Plot and says it has data once its Report runs, after the Changes are published. It is not an access refusal. Only someone who could edit the Parable’s Plots, with the Parable edit permission and Publishing or Primary on it, can read anything else from there, such as Provider Pools or another Parable’s Plots. Anyone else’s SQL that names something other than the Parable’s Plots is refused with HTTP 403 and PA-AU-019 before it runs. If your role reads Provider Pools, as Provider Pools Reader, Data Access and Workspace Administrator do, run the SQL outside the Parable instead. Under View as, SQL reads only the Parable’s Plots, as the person being previewed, whoever may edit. A refusal for your role, your seat or a Policy, a Pane’s included, is HTTP 403 with PA-AU-019. A commitId that names Changes nobody has published yet is refused with HTTP 409 and PA-AU-028. SQL sent with a Plot read stays limited to people who could save it, as above, because it replaces the Plot’s SQL. A constructed preview and a named read both apply their Pane’s Policies and their actual inputs’ restrictions. Restrictions that must apply to every read of a Provider table belong in that table’s Provider Policy. Invalid or unsupported filters refuse execution rather than returning unfiltered rows. For a Provider table no Provider Policy narrows for the reader, simple column-to-value comparisons reach the storage reader. A comparison on a partition column, such as the dt load date, lets the reader skip partitions that cannot match. A comparison on any other column, including an event-time column, lets the reader skip row groups whose value ranges cannot match; how much that saves depends on how the data was written. The query still applies the complete filter after the source check. A table a Provider Policy narrows for the reader keeps its filtering boundary: the query’s own conditions and limits do not run ahead of the Policy’s conditions, which the reader applies itself and can use to skip data. Only a direct comparison qualifies; a column inside a cast or function skips nothing. A source read can run in parallel across its files. While it runs, the source’s access is rechecked at least once a second and again as each parallel part finishes. The parallel parts share these checks, and so does every other place the query reads the same source for the same person, such as both sides of a self-join or a UNION. A part reading rows uses any check that began within the last second, and a part’s final check is always one that began after the part read its last rows. A revoked grant or a changed Policy fails the query before it returns a complete result; a query that streams rows may already have delivered up to a second of rows read after the change. SQL that reads another Plot’s stored rows checks them against that Plot’s output contract before any Policy, filter, limit, or aggregate applies. Where the check runs depends on what wrote the rows:
  • A Plot’s own table and a Report run’s output are written by query-runner. When the run holds the Plot’s contract, query-runner checks every row against it before it writes. A run whose rows break the contract writes nothing and fails with QR-QU-011, and the Plot keeps the output of its last successful run. A Report run and a manual Plot run hold the contract; a scheduled Plot run does not yet (PARABLE-5289) and writes unchecked.
  • The read of those rows makes the same check, whoever wrote them. It checks every value in each column the query reads whose contract says more than the column’s type, such as a literal, a scalar format, a bound, a required value, or a Date’s precision. This includes columns used by Policies, filters, joins, grouping and aggregation. An independent column the query never reads is not read from storage or checked, so COUNT(*) reads no column. Contracts that relate columns retain whole-row checks. A column whose type already satisfies its contract, such as an optional string, needs no value check.
  • A Report artifact was checked row by row when it was written. Its read confirms the contract, the stored bytes’ digest, and the row count before it opens the file, and does not check the rows again.
A read of a Plot’s own table or a Report run’s output skips the row check when the commit that wrote the table’s rows replaced the whole table and records the contract the read expects (parable.outputContractHash). It then reads only the columns the query selects, and COUNT(*) reads none. Compaction and VACUUM change no row, so the record holds after them. A table with no record, with a record for another contract, or whose last write kept earlier rows is checked as above. So is every read of a Report output that keeps embed() vectors beside its table: the record does not cover them. The record vouches for the contract, not for the version of the check that ran: when a later release tightens a scalar’s rule, rows written before it are served unchecked until the Plot runs again. The validation node runs before Policies or caller operators, permits parallel file reads, and prevents counts from being answered from file statistics. The reader validates the complete stored column schema once before scanning. Value checks use the native Arrow validator, with JSON validation for contract features that need it. Query-runner’s write-time validation remains tracked in PARABLE-5194. A view’s rows cross the same two boundaries as stored rows: the values of each column a query reads are checked against the view’s output contract, and the view’s Pane Policies (view.preferences) apply for the reader before any predicate of the reading query, in every Plot or Pane that reads it, directly or through other views. The view’s filter Policies apply where it is read inside a Plot they do not also cover, and its own filters wherever it is read; a filter that covers both meets the rows above the outer Plot. A view computes only the columns a query reads. The reading query’s own filters run on the checked rows, so a filter never hides a value that breaks the contract. The reader runs the view’s SQL under their own access to its sources, as for any Plot. Reading a view’s rows does not need view.plots: its SQL never reaches the reader, and a view that fails to plan is named without its SQL. A view that so far exists only in a set of Changes follows its Pane’s Policies as that draft sees them. In a Report run, the run’s principal applies each view’s Policies when the run is planned, its captured authority includes each view, and a reader of the run’s output must still pass each view’s Policies. A Report run reads its inputs as its principal: the person who chose Refresh, or the Workspace for a scheduled run. SQL outside a Report reading that names a Pipeline reads that run’s output, so the reader must still have the run’s access to every source it read, the same check a Report reading makes. The run’s artifacts.* and workspace.* tables have no Policies, so they add nothing to this check. Before the first row, each source’s current Policies for the reader must cover the filters the run was produced under. A Policy that adds or changes a filter the run did not have, or an input the reader can no longer see, refuses the read with Report data or its current input authority is unavailable. A source filter that uses request_principal() admits only the run’s own principal. While the read runs, the check runs again at least once a second and as each parallel part of the read ends, the same cadence as a source read. The parts share these checks, and a part’s final check is always one that began after the part read its last rows. A Policy that changes during the read stops it before the query returns a complete result; a query that streams rows may already have delivered up to a second of rows read after the change. A count or an empty result is checked the same way.

Stability

A Report run is a graph of its member Plots. Capture records which members each member reads, from the Plot dependencies planned when the Parable was published. A member runs only after every member it reads succeeded, and it reads the table that member wrote in this run, at the version it committed; every other input is read as of the run’s AS OF time. One query plan of a run holds at most 50 levels, and a view counts inside the plan that reads it, so a Plot that composes many views stays under that limit by storing the steps it builds on as members. A Plot only read live is not held to the limit. Members that read each other in a cycle are refused. A member may read a view of its own Parable, which the run computes inside its own plan, and a Plot outside the Report. Ordinary consumers of completed pipeline outputs support Current and explicit Report readings. A Report run’s captured definition supplies the planned input schemas, while current Policies authorize the recorded data. Contract validation and current-authority checks remain active through result streaming and completion, including empty results. Storage maintenance never removes a stored Report output. Each output is written once, beside the tables that compaction and VACUUM rewrite, and a read refuses an output whose stored bytes no longer match the run’s record. Retention alone decides when an old run’s output is deleted. Provider input replay is not yet available. A historical result whose Provider filter uses request_principal() requires the captured principal, including email. Source filters using now(), current_timestamp, current_date, full parable_context(), or another execution-dependent function refuse both Report materialization and historical reads. Use Current data for those filters until input replay is available. These limits matter when assembling a real classification pipeline; having SQL or a model UDF available does not itself connect that flow. Historical source admission follows immutable Plot identities. Retired, replaced, or ambiguous identities refuse. Current access must cover every captured contributor: unchanged filters or removed restrictions can qualify; new restrictions require replay and refuse. Once a read starts, any change to its admitted authority aborts that read before completion. A published Version never changes, so web-api compiles a Version’s Plots once and reuses the result for later reads of that Version. A read of a stored output also checks the output against the Version whose Report run stored it, and takes that Version from the same compiled Versions. It keeps up to 64 Versions and drops the least recently read one when it needs room; nothing expires by age. Every read still checks the reader’s seat, membership and the Pane’s Policies, before the Query Layer runs the read and again after it answers, with the run the reply came from. A read that expects another contract than its Plot’s is refused with a conflict, whether or not the Version is compiled. A read of Changes, or of a line in Personal mode, compiles its Plots on every read, because a revalidation, or a save to the ref the Changes build on, can change that tree without changing its revision.