Model Data
Data models & relationships
A data model sits between your raw data sources and your dashboards: a curated semantic layer of tables joined by explicit relationships, enriched with calculated fields, hierarchies, and SquareX measures. Dashboards built on the same model share its joins, security rules, and definitions, ensuring metrics stay consistent and governed across your entire workspace.

Create and manage models
Section titled “Create and manage models”From Data Models (/models):
- The list: every model of the workspace as a card, with its version (
v3), Certified or Draft, its table and field counts, its source and when it was updated. Search it (Search models…), sort it (Recently updated, Name (A-Z) or Most tables) and page through it. A card’s Edit, Export and Delete buttons act on that model; Delete asks first (Delete Data Model: the model, its tables, fields and relationships go, and dashboards built on it stop working). - New Model: Model name (up to 255 characters), the Data Source the model reads (each model reads exactly one data source of the workspace), and an optional Description (up to 2,000 characters), then Create.
- Model Selector & Renaming: Switch between models on the canvas using the header dropdown or rename an existing model directly via Rename.
- Model Blends: Access the Blends workspace (
/blends) from the model toolbar to join tables across different data models. - Import & Export Definitions (ALM): Export a model’s complete definition as a portable JSON file, or Import one (Import a DataSquares export) to replicate a model across environments. Importing is part of Enterprise.
Create a model, step by step
Section titled “Create a model, step by step”Name the model, choose its source and import the tables it needs.
Open Data Models. Each card is a model, marked Draft or Published. Search the list or sort it by recent changes or size.

Choose New Model. Name it, pick the data source it reads from, and add a short description.

The model opens on its canvas. It is empty: add the tables it is built from.

Choose Import Tables. Pick the schema, then tick the tables the model needs. Here: orders, customers and regions.

Each table appears on the canvas with its columns. The model stays a Draft until you publish it.

Import tables & schema discovery
Section titled “Import tables & schema discovery”- Import Tables: In the model editor toolbar, click Import Tables. Select a schema to list its tables along with estimated row counts (
~120,000 rows). - Schema discovery: If the source has not been profiled yet or schemas are missing, click Run Schema Discovery. Discovery runs as an asynchronous background job with live polling.
- Table selection: Check the tables you need (or click Select All) and click Import N Table(s) to bring them onto the canvas as interactive nodes.
View data, Import Tables and Sync Schema need permission to edit models, and a model whose source is set.
Import also looks for the joins between the tables you bring in. The ones the source declares — a foreign key, or a column with the same name as another table’s only primary key — are created for you and drawn as dashed green lines: detected, not yet confirmed. Weaker guesses, such as a region_id that looks as if it belongs to a regions table, are not created; they wait under Suggested relationships. The confirmation after the import says how many of each there are, and an import never gives two tables that are already connected a second route.
Visual relationship canvas
Section titled “Visual relationship canvas”The model editor provides an interactive canvas powered by @xyflow/react:
- Table nodes: Display the table’s name and row count, and each column with a short type label; a primary key carries a key mark (🔑). Click a column to open its Field Configuration panel.
- Drag-to-connect: Drag from a table’s handle (or its header) onto another table to open the Define relationship dialog. Relationships join tables; you choose the columns in the dialog.
- Relationship edge selector: Click an existing edge to change its cardinality, switch it off with Active, confirm it if it was detected, or delete it. Deleting warns you that charts joining through those tables will stop combining them.
- Line styles: A solid line is a relationship you defined or confirmed. A green dashed line that moves was detected from the source’s keys and waits for you to Confirm it or delete it. A faint, finely dotted grey line is inactive. Each line runs from the one side to the many side and carries its cardinality and join type, such as
1:N LEFT.
Defining relationships
Section titled “Defining relationships”Drag from one table to another and the Define relationship dialog opens. Choose a column on each side and DataSquares works out which table is the one side — the table each of whose rows can match many rows of the other — and says so under the columns, for example customers one → orders many, with its reason. It goes by, in this order:
- A declared foreign key between the two columns:
orders.customer_idis a declared foreign key tocustomers.customer_id. The table that holds the key is the many side. - A primary key:
customers.customer_idis the primary key ofcustomers. - The values themselves, when no key settles it. The dialog compares each column’s number of rows with its number of distinct values, and the column with a different value in every row is the one side: Every value of
customers.customer_idis different. Create relationship waits while it counts. If both columns repeat, neither table is the one side and the cardinality starts at many-to-many. - The way you dragged, when there is nothing to go on: No key is recorded and the values could not be checked. The table you dragged from is taken as the one side, and the line says so.
When the keys point at one obvious pair of columns — a declared foreign key between the two tables, or a column with the same name as the other table’s primary key — the dialog starts with them chosen, and it starts Cardinality from what it found: one-to-many, or one-to-one when both columns have a different value in every row or the foreign key is also its table’s own primary key.
Swap reverses the direction. If you swap against the evidence, the line under it says that you did and what the evidence was. The relationship is stored with the one side first, so its line on the canvas always runs from the one side to the many side, whichever table you dragged from, and Join type reads the same way: with Left, every row of the one side is kept, matched or not. The dialog spells out each choice under the selector.
When creating a relationship, configure:
- Source & target columns: Join keys must be physical source columns. The dialog offers visible columns only, and a calculated field is refused (Calculated fields cannot be used as join keys — pick a source column.). A table can’t join to itself.
- Cardinality:
1:1(One-to-one)1:N(One-to-many)N:M(Many-to-many)
- Join type:
Inner,Left,Right, orFull(Left unless you change it). Charts use it with one rule on top: a lookup never adds rows to the table a chart measures (details).
Then choose Create relationship. Once it exists, the canvas changes its cardinality and whether it is active, but
not its columns or its join type: to change those, delete the relationship and define it again (or use
PUT /api/models/{modelId}/relationships/{relId}).
Switch a relationship off
Section titled “Switch a relationship off”Click a relationship’s line on the canvas and its selector opens at the bottom of the canvas: Relationship Type, an Active switch, Confirm (for a detected one), Done and Delete. Turn Active off and click Done to keep the relationship but make charts ignore it. It is the way out of a second route between two tables — a table reachable by two active paths is refused in charts, see Joins & fan-out — that does not delete a join you may want back. Turn Active on and click Done to bring it back.
An inactive relationship keeps its keys and stays on the canvas, drawn as a faint, finely dotted grey line that does not move and is labelled inactive. Relationship Health, on the Lineage tab of Data quality, lists it with an Inactive badge.
Suggested relationships
Section titled “Suggested relationships”Relationships that a model’s keys imply but nobody has decided on wait under Suggested relationships. A button of that name, with the number waiting, appears in the toolbar when there are any, and opens a panel with a card for each:
- the pair, such as
orders.region_id → regions.region_id, how sure the detection is, and what it went on; - a plain sentence about the sides: Each
regionsrow can match manyordersrows.; - Accept, which creates the relationship (one-to-many with a left join, or one-to-one when the key is also its table’s own primary key), and Dismiss, which closes the suggestion — that pair of columns is not suggested again.
Find again in the panel, or More → Find relationships in the toolbar, reads the tables’ keys once more; it is how a model imported earlier gets its suggestions.
When the two tables are already connected, the card says so — regions and orders are already connected. This would be a second route, so it is added inactive. (the one side first) — and Accept adds the relationship inactive, so accepting it cannot make a chart that works start failing. If the new route is the one you want, switch it on and switch the other route off.
Anyone who can open the model can read the list; accepting, dismissing and looking again need the models create permission (and More → Find relationships in the editor also needs permission to edit models). These suggestions are separate from the Suggestions tab in Data quality, which holds the recommendations of a data-quality scan.
Data viewer (Inspect rows)
Section titled “Data viewer (Inspect rows)”Double-click any table node on the canvas (or click View data in the toolbar) to open the Data Viewer:
- Fail-closed RLS: Previews run through the authenticated
/api/queryengine with row-level security enforced. - Row limit selection: Choose between 100, 500 (unless you change it) or 1,000 rows.
- Server-side column filtering: Filter by column with operators (
contains,=,≠,>,<,is empty,is set). - Client-side instant search & sort: Search across loaded rows and sort any column with windowed rendering.
Sync schema
Section titled “Sync schema”When upstream database schemas change, run the source’s schema discovery again (Run Schema Discovery in Import Tables), then click Sync Schema on the toolbar. It reads the source’s last discovered schema, not the live database:
- A column new in the source is added to its table as a field.
- A field whose column is gone from the source is hidden, not deleted, so what reads it says so instead of breaking.
- An existing field keeps its data type; the table’s stored column list and row count are updated.
- A table gone from the source is reported, not removed.
- Relationships, measures and calculated fields are kept.
The message says what it did: Schema synced — N tables refreshed, N new fields, N vanished fields hidden, N tables no longer in source.
Hierarchies
Section titled “Hierarchies”Click Add Hierarchy to define multi-level drill paths on a table (e.g. Region → City → Store or Category → Subcategory → SKU): give it a Hierarchy name, choose the Table, then its levels from the top (Level 1, Add Level), and Save. Charts on the model automatically enable drill-down navigation along these paths. Saved hierarchies show as chips under the canvas; a hierarchy can be deleted there (Delete hierarchy? — charts that drill through it stop offering the next level) but not edited in the editor: delete it and add it again, or use the Semantic Model API.
Measures & calculated fields
Section titled “Measures & calculated fields”- Calculated fields: Define row-level column expressions directly inside table field configurations.
- SquareX measures: Click Measures (
ƒx) in the toolbar to open the SquareX measure builder for model-level, filter-aware calculations. Renaming a measure renames it in the governed metrics that name it, and a measure a metric names can’t be deleted.
Row-level security (RLS)
Section titled “Row-level security (RLS)”A row-level security policy limits which rows of one table people see. Open More → Security to list
the model’s policies, each with the table it filters and who it applies to, and to add, edit or remove them.
A model’s policies, and those it inherits from other models, are shown only to people whose role may create or edit
data models (anyone else gets a 403 from the API); they filter everyone’s queries all the same.
A policy has:
- Table: the model table it filters.
- Policy name: a name for the policy, shown in Security.
- Filter expression: a SQL condition on that table’s columns, such as
region = 'US'ordept_id IN (1, 2). The people the policy applies to see only the rows it keeps. An expression that fails the safety check is refused when you save, because as written it would hide every row. - Rules on the person’s attributes (optional): keep only the rows whose column holds one of the person’s own values for an attribute, such as region is one of the person’s region values. With a rule, the filter expression becomes an optional Extra condition (optional), kept as well; see Rules on a person’s attributes.
- Applies to: who it limits. Choose Everyone (everyone who can query the model, company admins
included), or Chosen roles and people:
- Members: everyone in the company who isn’t a company admin, workspace admins included.
- Company admins.
- Specific people: anyone who can query the workspace, which means its members and the company’s admins. Search by name or email.
A policy binds its data source, not only its model: it applies whenever any model over the same source reads its table, beneath every chart, metric, export, drill-down, deck, embed and Data Viewer query. A model built later over the source, or a table added to a model, is filtered by it too. Dashboard filters and measures can’t get around it, and the SQL editor refuses raw SQL for anyone a policy restricts. AI output saved on a dashboard is not a query, so it is hidden from them instead: a card’s AI summary and the chart of an AI snapshot card.
How a policy matches people
Section titled “How a policy matches people”- Roles are company roles. A policy matches a person’s role in the company (member or company admin), never their workspace role. There is no Viewer or Editor target: to limit the workspace’s viewers, choose the people.
- Groups can’t be targeted yet. Choose the group’s people instead.
- API keys act as their owner. A key gets exactly the rows its owner gets.
Rules on a person’s attributes
Section titled “Rules on a person’s attributes”One policy can give each person their own rows: a rule compares a column of the table with the values the person holds for an attribute, such as their region, department or cost center. A company admin sets people’s attributes under Admin → Users → Attributes, or your identity provider sends them through SCIM.
In the policy dialog, under The person’s own values, choose Add an attribute rule, pick the Column and type the attribute’s name (up to 64 characters).
The names people in the workspace already hold are offered, each with how many people hold it, never
their values. A name is lower-case letters, digits and underscores, starting with a letter (region,
cost_center); another is refused with that reason. Security lists each policy’s rules in words.
- Values match exactly. For a number column, a value that is not a number is ignored; for a true or
false column, only
trueandfalsecount. - Every rule, and the extra condition, must hold for a row to be kept by the policy.
- No value, no rows. Someone the policy applies to who holds no value for an attribute it names gets no rows from this policy. Other policies on the table that apply to them still add theirs.
- Changes apply at the next query. A person’s values are read on every query, so a change under Admin → Users or from your identity provider counts from the next one. A result kept for one person is never served to another with different values.
- The column must be one the model has a field for, not a calculated field; another is refused when you save, naming it.
- The preview counts people by the attribute names they hold, so someone without a named attribute shows as seeing nothing.
- Definitions carry the rules. Exporting a model carries its policies’ rules, and importing it keeps them.
How policies combine
Section titled “How policies combine”- A table no policy applies to is not filtered. Row-level security is opt-in: someone no policy applies to sees every row of the table.
- Several policies on one model table add up. A person sees the rows that any policy applying to them keeps.
- Policies on the same source table all apply. Policies match the source table they filter (schema and name, ignoring case). Where several model tables over one source table have policies, a person sees only the rows each set allows: a model’s own policies can narrow, never widen.
- A policy whose table can’t be matched refuses the queries it would narrow, rather than read unfiltered.
- Public links and a deck’s links show nothing from a table a policy filters, whichever model reads it.
- Whoever a policy restricts can’t change the policies. Anyone a policy on a data source restricts can’t add, change or remove that source’s policies, on any model over it: that includes pointing the table a policy is written on at another table, deleting a model that carries a policy, restoring a version of one, or importing a model with policies onto the source. A company admin can.
Policies from other models
Section titled “Policies from other models”Because a policy binds its source table, Security also lists, under From other models, the policies written on other models over the same source that narrow this model too. Each is marked Inherited and says where it comes from (From Sales, on its Orders table), with who it applies to, which of this model’s tables it filters here (Filters Orders here too: both read public.orders.) and the rows it keeps. The model’s name is a link that opens its Security: an inherited policy is changed only on the model it is written on. One whose table can’t be matched to this model’s tables says so, as a query here that it would narrow is refused.
A model with none of its own says No policies written on this model. above them. If they can’t be read, Security says Couldn’t load the policies other models over this source apply here., with Try again, so an empty list never reads as none. In the Catalog’s lineage, the model’s A policy applies names those models too (see The lineage chain of a dashboard or a metric).
See who a policy covers before you save
Section titled “See who a policy covers before you save”While you add or edit a policy, the dialog shows what it would do once saved: Applies to N of M people, and how many people would then see every row of the table, only filtered rows, or nothing, counting every other policy on the same source table, in this model or another. Covered and Not covered list the people on each side, with you marked (you). The preview warns when a policy restricts no one, and when someone would see no rows.
Everyone includes company admins, so a policy can hide rows from you as its author. You can still edit or remove it.
What is refused
Section titled “What is refused”More → Security needs permission to edit models; adding a policy (Add RLS Policy) needs the models create permission, Edit the edit permission and Remove the delete permission (Delete security policy? asks first). A policy needs a filter expression or an attribute rule, and an expression that fails the safety check is refused (Invalid RLS filter expression: … As written it would deny all rows for this policy.).
Saving refuses a target that can never match anyone, and the message names it: a role other than member or company admin, a person outside the company, or a group. You can still choose a person who is deactivated, or a company member who isn’t in this workspace yet: the policy applies to them if they come back or join.
Policies saved before these checks keep working. A target in them that can never match is flagged in Security: edit the policy to remove it. When you import a model definition, a policy that names people outside your company is skipped, and the import message lists it.
In the Semantic Model API, appliesTo is an empty list for everyone, or entries such as
{ "type": "role", "value": "member" } (or company_admin) and { "type": "user", "value": "<user id>" }.
A refused target comes back as a 400 whose details.targets names each one.
Add a hierarchy and a security policy, step by step
Section titled “Add a hierarchy and a security policy, step by step”An example: a channel-to-status hierarchy and a policy that limits rows to two regions.
Choose Add Hierarchy. Name it, pick the table, then add a level for each field, broadest first. Viewers can then drill from channel down to status.

Open More, then Security. Row-level security policies limit which rows a person sees. Choose Add RLS Policy.

Name the policy and write the filter as a condition on the table's columns. Under Applies to, choose who it limits: everyone, a company role, or people (here the demo viewer). The preview shows who it covers and what everyone sees on the table. Then save.

Security lists each policy with the table it filters and who it applies to. Edit changes either. A policy applies every time the model is queried.

Find sensitive columns
Section titled “Find sensitive columns”DataSquares looks for columns that hold personal or secret values and proposes a sensitivity label for each, so they are masked for people who may not read them. It does this with fixed rules, not with the AI: no value is sent to the AI, kept, or written to a log. Only the counts of how many values matched are kept.
| Values that look like | Rule | Label proposed |
|---|---|---|
| Aadhaar numbers | 12 digits, the first from 2 to 9, with a valid check digit; written whole or as three groups of four | pii |
| PAN | five letters, four digits and a letter, the fourth letter naming the holder type | pii |
| UPI IDs | a name, @ and a payment handle without a dot, such as name@okicici |
pii |
| Email addresses | a name, @ and a domain with a dot |
pii |
| Phone numbers | an international number after +, an Indian mobile number, or a national number written with its groups |
pii |
| GSTINs | a state code, the holder’s PAN, an entity number, Z and a valid check character |
confidential |
| Card numbers | 13 to 19 digits a card network issues, with a valid check digit | restricted |
A column is proposed when at least 20 of its values were read and 90% of them match one rule. When the column’s name
says what it holds (aadhaar or aadhar, pan, gstin, upi, vpa), half of them is enough for that rule.
What is checked, and when
Section titled “What is checked, and when”- Columns: visible text and number fields without a label. Calculated fields, hidden fields and fields of hidden tables are skipped. Columns of types that hold no such values (JSON, arrays, binary data, IDs, places) are not read.
- Same names: a column whose name already carries a label elsewhere on the same source (in another table, in another model, or in a Warehouse copy of a table) is not read either. A label masks its own column wherever it is read, in every model of its source and its Warehouse copies, but never another column that only shares its name. The dialog counts these columns and names them (the first 200, as table.field), so you can label them yourself in the field panel. A proposal made before the other label was added stays in the list for you to decide.
- Names: the check reads only columns and tables whose names are letters, digits and underscores. A column with another name (a space or a hyphen, for example), or in a table with one, is not checked. The dialog counts these columns, so you can label them yourself in the field panel.
- How: once per table, DataSquares reads up to 200 rows in which at least one of those columns has a value, within the source’s query time limit. A text value longer than 254 characters is left out by the query itself, since no rule matches one that long. Each value is checked and dropped at once.
- When: in the background. A new column is checked about a day after its table was last checked, every column again each week, and a table that could not be read is tried again a week later.
- Which sources: uploaded files, sheets and other copies kept in DataSquares, and the Warehouse, are checked automatically. The check of a Warehouse dataset is a system read: it does not count toward your Warehouse query hours. A live database is checked only once a company admin turns on Check live databases (in the same dialog, on a model that reads one): it runs one small query per table on your database, about once a week.
Review the proposals
Section titled “Review the proposals”- Open the model and choose More → Sensitive columns. The top of the dialog says when the columns were last read, how many tables the last check could not read, how many columns it cannot check because of their names, and which columns it does not check because a column of the same name is labelled elsewhere.
- Each row shows the field, its table, what its values looked like with how many matched (for example Email addresses: 196 of 200 values), and the label approving adds.
- Tick the rows you want, or narrow the list with Find a column and tick Choose all shown.
- Click Approve to add the labels, or Reject. You can decide up to 200 at a time.
- Anyone who can open the model sees the proposals. Approving and rejecting need permission to edit models.
- Approving only adds a label. A field that carries a label by the moment its label would be added keeps it, because only a company admin can change or remove one (who can change a label).
- Approving several columns of the same name, in one go or one at a time, labels each of them.
- A proposal that someone else decided, or whose column changed, while you were approving is left as it was. The message after Approve says how many, and the list then shows what is still proposed.
- A rejected column is not proposed again for 90 days.
- Each approval and rejection, and each change to Check live databases, is recorded in the audit log.
Through the API: GET /api/classification/proposals?modelId=… lists a model’s proposals, and
POST /api/classification/proposals/approve or /reject with { "modelId": "…", "ids": [ … ] } decides up to 200.
A request is checked whole before anything changes: a proposal that is not the model’s answers 404, and one decided
already, or no longer proposed (its column or table was hidden, deleted or made calculated since, or its live database
is no longer checked), 409, and nothing is changed. Each approval is then saved with its label on its own: should one
fail, the approvals before it stay, the rest stay open, and the audit log records what was done. Approving answers
approved (labels added), superseded (fields that already had a label and kept it) and notApplied (proposals left
as they were because they were decided or changed while the request ran). The list’s model part carries
lastScanAt, unreadTables, uncheckedColumns and sameNameColumns, with sameNameColumnList naming the first 200
same-named columns (fieldId, fieldName, tableName). A company admin turns live databases on or off with
PUT /api/classification/settings and { "liveSources": true }.
Publish a model
Section titled “Publish a model”Publish when the model is ready for dashboards.
A model starts as a Draft. Choose Publish when its tables, joins and fields are ready for dashboards.

The Published badge appears next to the model name. Dashboards and reports can now be built on it.

Back in the list the model card carries the same badge. Use Export on a card to download the model definition.

Version history & atomic restore
Section titled “Version history & atomic restore”Click Publish to release the current draft to dashboards, bumping the version number and creating an immutable snapshot.
- Quality gate: On Business and Enterprise, publishing is held while an active critical data-quality test on the model or its source is failing or could not run: Publishing held by data quality lists them. Someone who may publish can still go ahead with Publish anyway, giving a reason (up to 500 characters) that the audit log records.
- Version history (More → Version history): Inspect previous published versions with timestamps, version numbers, table counts, and field counts; the newest is marked Latest, and Restore brings one back. A model never published says Publish your model to create restorable version snapshots.
- Atomic restore: Restores a snapshot in place in a single database transaction. All persistent field IDs remain unchanged so connected dashboard cards, filters, and data quality tests stay attached without breaking.
Changes and the schedules that read a model
Section titled “Changes and the schedules that read a model”A scheduled report or dashboard is sent as its creator sees it. When someone other than a schedule’s creator or a workspace admin changes a model the schedule reads — a field, measure or table added, changed or deleted, a relationship, a table’s transform steps, a schema sync or table import, or a restored version — the schedule pauses until its creator checks what it now sends; see Scheduling. A field that a card, a report, a dashboard’s filter pane, a schedule’s filters or a metric still uses can’t be deleted: remove or repoint those first.
Related
Section titled “Related”- Joins & fan-out — cardinality mechanics and preventing fan-out errors.
- Calculated fields — row-level expressions on table columns.
- SquareX measures — aggregate measure expressions and context transitions.
- Data quality — test suites and relationship health on top of data models.
- The AI scope — choose which tables and fields the AI may use on a model.
- Prep for AI — how ready a model is for the AI, AI-drafted descriptions to review, and where each join comes from.
- Saved queries — using model bindings inside saved SQL queries.