Page data model
A Page declares the data it needs by name, and the spec binds components to it. The host runs those queries live, for each viewer, scoped to what that person is allowed to see. No tenant data is ever stored in the Page itself.
Named queries
A Page does not fetch data, it declares it. Alongside the spec, a Page ships a set of named queries (its declared_queries). Each one has a name and a definition; the spec binds a component to it by that name.
{
"declared_queries": [
{
"name": "deals_by_stage",
"source": "sql",
"sql": "SELECT stage, COUNT(*) AS count FROM interactions WHERE interaction_type = 'deal' GROUP BY stage"
}
]
}A component then binds to deals_by_stage and gets its rows at render time. A Page can only ever reference the queries it declared, by name — there is no way for a Page to run an ad-hoc query.
Bindings: $query and $value
The spec connects a component to a query with a bindingin its props. There are two shapes:
{ "$query": "deals_by_stage" }resolves to the wholerowsarray — what aTableor chart binds itsdataprop to.{ "$value": { "query": "deals_by_stage", "field": "count", "row": 0 } }resolves to a single scalar cell — what aStatbinds its value to.
{
"type": "Table",
"props": {
"data": { "$query": "deals_by_stage" },
"columns": ["stage", "count"]
}
}At render time the host resolves every binding against the results of the declared queries and hands each component its data. A prop that is not a binding is a plain literal (a title, a column key, a color).
SQL is tenant-agnostic
Notice what the SQL above does not contain: a business_id filter. Page SQL is written tenant-agnostic on purpose. The host injects the workspace scope, and the per-viewer access predicate, when it runs the query. The author writes the shape of the question; the host pins it to the right tenant and the right rows.
This is the same idea as everywhere else in Amdahl: the data-scope rules you set in Settings → Data → Data Access apply here too. Two teammates opening the same Page may see different numbers, because the host scoped each one’s queries to their own access. The Page code is identical; the data is per viewer.
The same validator as data.query
Every source: "sql"query a Page declares runs through the exact validator the Data Explorer’s data.query operation uses (see Data). That means a Page query is held to the same rules:
- SELECT-only. Reads, nothing else. No
INSERT,UPDATE,DELETE, or DDL. A Page can show your data, never change it. - Table-whitelisted. Queries may only touch the approved data views (the unified
interactionsview and its siblings). A query against anything off the whitelist is rejected.
Because Page SQL and Explorer SQL share one validator, a query that is safe to run in the Explorer is safe to declare on a Page, and a query that would be rejected there is rejected here too.
Beyond SQL: cluster and knowledge-base sources
source: "sql" is the common case, but a declared query can draw from two other host primitives instead, so a Page can bind to more than just rows of SQL:
source: "cluster_search"— a semantic search over a corpus. Setqueryto the search text andtargetto the corpus (for example the interaction clusters). The rows are the matching clusters.source: "kb_search"— a search over the workspace Knowledge Base. Setqueryto the search text; the rows are the matching documents.
{
"declared_queries": [
{ "name": "themes", "source": "cluster_search", "query": "pricing objections", "target": "interactions" },
{ "name": "playbooks", "source": "kb_search", "query": "discovery call checklist" }
]
}Whatever the source, a component binds to the query by name exactly the same way — $query for the rows, $value for one cell. The source decides how the host fetches the rows; the binding does not care which primitive produced them.
Runtime inputs with params
A query may also declare named params— runtime inputs the Page supplies when it renders, referenced as @name in the SQL. Declare them as a map of name to a type hint on the query; this is how a Page takes a date range or an account filter at view time without rewriting its SQL.
Nothing is stored
A Page holds no tenant data. It stores the spec and the query definitions, never any rows. Every time someone opens the Page, the host runs the declared queries fresh and feeds the results in. So the numbers are always current as of the moment you look, and a Page you share with a teammate shows their scoped data, not a snapshot of yours.
How it fits together
- The Page declares named queries up front.
- A viewer opens the Page; the host renders the spec tree.
- The host resolves each
$query/$valuebinding to the query it names. - It runs that query, injecting the tenant filter and the viewer’s access predicate, and returns the rows.
- The bound components render them. Repeat per viewer, live, every time.