If you've configured connectors before, you'll remember place mappings: a hierarchical mapping table where you matched source-system rows to a Site → Building → Floor → Space → Thing tree, with a separate "provision" step and template-based mapping import. Entities replaces all of that.
What's changed:
| Old way (place mappings) | New way (Entities) |
|---|---|
| A flattened nested Site→Building→Floor→Space tree you filled in | A flat table (Grid), one screen per entity type |
| Separate "provision" step, then "map" step | One screen, one flow — Import, then Save |
| Mapping a Space to a Floor was a separate column/target | The Space simply has a Floor cell you pick |
| CSV import for bulk changes | Rules written as a TypeScript function (optional) |
One screen per "source type." A connector groups its data into source types — the names the source system uses for its own things (for example Airthings exposes building, space, and thing). Each source type gets its own section in the connector's settings, and each section is its own Entities screen with its own Grid and Rules. So a customer might see a Buildings screen, a Spaces screen, and a Devices screen, and work through them one at a time.
During a migration you may see both. A connector that's being moved from the old model to Entities can temporarily show the old mapping table and a red "Entities Migration" section. Migration is a one-time rename that keeps all existing data and history intact. Documentation on migration to be released soon.
Two tabs at the top:
Two buttons at the top-right:
Above the Grid there's a smart search bar which can narrow what an Import pulls in. Depending on the connector, there may be filter fields (for example a Site picker or a sensor-type selector). Some connectors have no filters. Previous filter selections are saved between imports.
When no entities have been imported, an empty Grid at the bottom reads "No entities found. Click Import to load entities from the connector."

Import fetches the current, live list of entities from the source system and lays them out as rows in the Grid. Each row is one entity — one building, one room, one sensor.
On import, for each row the connector provides:
Nothing is written to Mapped yet. Import only loads and shows — Save is what commits.
Import reflects the source system exactly. The list you get is the complete current state of that source type. If something exists in the source, it shows up; if it doesn't, it won't. That matters a lot for re-imports — see the next section.
Import isn't a one-time action. A customer can click Import again whenever the source system changes (new rooms added, a sensor decommissioned, a name corrected). When they do, the screen compares the fresh import against what's currently saved in Mapped and highlights the differences.
You'll see a change banner / count summarizing the delta, using three categories:
An entity that the source system no longer returns is shown struck through (a line through the whole row) in red, its checkbox unticked and disabled, and its cells read-only. The next Save will remove it from Mapped.
These "gone" rows can't be kept. If the source system dropped an entity, nobody — not the customer, not a merge rule — can re-select it to keep it. The only way to keep it is for it to reappear in the source system and be imported again. This is deliberate: Mapped mirrors the source.
Good news for customers: manual edits they made to a cell are remembered across re-imports. If a customer renamed a room and saved it, a later Import won't silently overwrite that name back to the source's version. (Two per-field flags — whether a cell is required and whether it must be unique — are refreshed on every import, because those are controlled by the connector, not the customer.)
Counts of any changes made sit at the bottom-left of the Grid:
| Count indicator | Color |
|---|---|
| 12 SELECTED | blue |
| 3 ADDED | green |
| 1 UPDATED | amber |
| 2 DELETED | red |
Only counts above zero appear. The bottom-right shows how much is loaded, e.g. 200 of 1450. Click any of these counts to filter the Grid down to those rows. For example, clicking 2 Deleted filters the Grid down to exactly those two rows. It is the fastest way to answer "what is about to be deleted?" The same counts appear in the Review Changes dialog, and clicking one there closes the dialog and applies the filter.
The Grid is editable. A customer can double-click into most cells and change the value — rename a space, set a floor level, pick which floor a room belongs to, and so on. Each column uses an editor suited to its type (a text box for a name, a number for a floor level, a searchable picker for "which floor").
Some cells don't hold plain text — they hold a link to another entity. For example, a Space has a "Floor" cell that points at the Floor it sits on. Editing that cell opens a tree picker of eligible places, and the customer selects one.
This is the single most useful thing to teach a customer. Every cell is color-coded by its source:
| What you see | What it means |
|---|---|
| Amber border | Unsaved edit — save to apply. |
| Dark-blue border, read-only | Set by a rule. Locked to manual edits. |
| Greyed text | Default value (placeholder) |
| No special styling | Value from the Graph. |
| Red border | Duplicate value in a unique field — must be unique to save. |
There's a legend behind the little help icon on the checkbox column header that explains these colors. If two apply at once, red wins over blue, blue wins over amber, amber wins over grey.
The first column of every row is a checkbox:
Save is the commit. It takes everything the customer has selected and edited and writes it to Mapped in one action.
Clicking Save opens a Review Changes dialog before anything is written. It shows:
The customer clicks Submit to proceed (or Cancel to back out).
On Submit, Mapped processes the whole batch at once:
After a successful Save, the freshly edited cells become "settled" (no more amber borders), and those entities are now live in Mapped — visible to the rest of the platform.
If a Save fails, the Save button turns amber and permanently displays: "Latest merge failed. Click here to try again." Clicking retries straight away, and no changes are lost.
Save is per-screen. Each source type (Buildings, Spaces, Devices…) has its own Import and its own Save. A customer works through and saves each screen.
1 . Open the connector → open a source-type section (e.g. Spaces).
2 . (Optional) set any filters above the Grid.
3 . Click Import → rows load into the Grid.
4 . Review the change counts if this isn't the first import.
5 . Tick the rows to include; double-click to edit cells as needed (watch the colors).
6 . (Optional) use the Rules tab to auto-fill — read Merge Rules for more
7 . Click Save → Review Changes → Submit.
8 . Repeat for the next source-type section.
Merge rules are the advanced configuration options for importing data into the Mapped platform from a connector. They let you automate what would otherwise be manual row-by-row work in the Grid: instead of ticking and editing hundreds of entities by hand, you write a small piece of logic that runs once for every imported entity and fills in values and/or ticks checkboxes automatically.
Merge rules are written in code (TypeScript), but you don't need to be a developer to use them. However you should be comfortable reading and editing a short function or working with an AI agent that can. The Rules editor helps you: it autocompletes, checks your syntax as you type, and shows you exactly which fields exist for the connector you're configuring.
A merge rule is a single function that the platform runs against one entity at a time, for every row in the Grid, every time you Import or preview. For each entity, the rule can:
Whatever the rule sets shows up in the Grid as rule-owned cells (dark-blue border, read-only) and, where the rule sets selection, a rule-locked checkbox (disabled). Everything the rule doesn't touch is left to the customer's manual edits and the source defaults.
Why use them? Common jobs:
A rule applies one decision across thousands of rows, consistently, and re-applies automatically on every future import. A rule can point one entity at another by giving one of three things: the target's mapping key, its Mapped id, or its identity. For example:
Copy1entity.setField('floor', { type: 'FLOOR', mappingKey: 'msrc://…' })
If the field only accepts certain kinds of target (a Floor cell only accepts floors), it rejects anything else as you type — the same restriction the Grid picker enforces.

Each Entities screen has a Rules tab next to the Grid tab. It's a code editor (Monaco — the same engine as VS Code) with:
The editor opens on this starter template, which does nothing until you fill it in:
Copy1 2 3 4 5 6 7// Merge rules — runs once per imported entity. export default function rules(entity: RulesEntity): RulesEntity { // Example: // entity.setField('name', 'Renamed') // entity.setSelected(true) return entity }
Always check the types.d.ts tab first. It's generated from the real entity for that source type, so it lists the actual field slugs (name, floorLevel, floor, …) and context keys the connector emits. Don't guess names — read them off that tab.

A rule is just a function with a body. Read it as a list of "for this entity, do X." The building blocks are a handful of entity.* calls, split into reads and writes.
| Call | What it gives you |
|---|---|
| entity.getRefId() | The entity's ID in the source system. |
| entity.getContext('key') | Extra data the connector attached (e.g. a wing, a floor number, a status flag). Keys are listed in types.d.ts. |
| entity.getField('name') | The entity's current value for a field (default, or a prior manual edit). |
| entity.getMappedId() | The entity's existing Mapped ID, if it's already saved. |
| entity.getOrgId() / entity.getConnectorId() / entity.getConnectorTypeId() | Fixed identifiers for the org and connector — occasionally useful for conditions. |
Review the types.d.ts tab for a full list of read calls.
| Call | What it does |
|---|---|
| entity.setField('slug', value) | Sets a typed field value. The cell becomes rule-owned (dark-blue, read-only). |
| entity.setSelected(true | false) | Ticks (include) or unticks (exclude) the checkbox. The checkbox becomes rule-locked. |
A rule always returns the entity at the end (return entity) so the platform can apply the changes.
Copy1 2 3 4 5 6 7 8 9 10 11 12 13export default function rules(entity: RulesEntity): RulesEntity { // 1. Only include "real" leasable rooms — skip service closets, risers, etc. const code = entity.getField('spaceCode') // read the room code, e.g. "L12-204" const isLeasable = typeof code === 'string' && code.startsWith('L') entity.setSelected(isLeasable) // tick it only if it's leasable // 2. If the source didn't give this room a name, fall back to its code. if (!entity.getField('name') && code) { entity.setField('name', code) // set the name to the code } return entity // hand the entity back }
Read as: "Tick every room whose code starts with L; for any of those missing a name, use its code as the name; leave everything else alone." That single rule then applies to every row on every import.
The workflow:
Include or exclude by a condition:
Copy1 2 3 4 5export default function rules(entity: RulesEntity): RulesEntity { // Include only occupancy sensors; leave everything else unticked. entity.setSelected(entity.getContext('deviceKind') === 'occupancy') return entity }
Fill a missing value:
Copy1 2 3 4 5 6 7export default function rules(entity: RulesEntity): RulesEntity { // Give unnamed devices a name based on their source ID. if (!entity.getField('name')) { entity.setField('name', **Device ${entity.getRefId()}**) } return entity }
Combine reads:
Copy1 2 3 4 5 6 7 8 9 10 11export default function rules(entity: RulesEntity): RulesEntity { const floor = entity.getContext('floorNumber') // Only bring in entities that report a floor, and stamp it into the name. if (floor != null) { entity.setField('name', **${entity.getField('name')} (Floor ${floor})**) entity.setSelected(true) } else { entity.setSelected(false) } return entity }
This is the question customers ask most, so it gets its own section. Every cell has a winning value, chosen from up to four sources in this priority order (highest wins):
1 . A merge rule — wins outright, locks the cell.
2 . An edit made in this session — typed but not saved yet.
3 . An edit saved earlier — survives re-imports.
4 . The value currently live in Mapped ← missing from the list above
5 . The source system's default.
In plain language:
These protect data integrity and win over everything, including a rule:
Rule of thumb: rules are for decisions you want made the same way every time; manual edits are for one-off corrections. If a cell is locked dark-blue, a rule owns it — change the rule, not the cell.
So you can iterate: tweak the rule → Save → Import later → the rule re-runs automatically.
If you maintain a reference of building identifiers, building names, and postal addresses, you can use a Grid Rule to automatically populate those values during import. The rule matches each building using its source-system Ref ID, then applies the correct name and postal address automatically. Using the building Ref ID is recommended because it is generally stable even if the building name changes over time.
| Building Ref ID | Canonical Building Name | Postal Address |
|---|---|---|
| <building-id-1> | Example Campus Building A | 100 Main Street, Springfield, MA 01103 |
| <building-id-2> | Example Campus Building B | 200 Main Street, Springfield, MA 01103 |
| <building-id-3> | Example Campus Building C | 300 Main Street, Springfield, MA 01103 |
Create an object that maps each building Ref ID to its canonical name and postal address.
Copy1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22const BUILDINGS: Record< string, { name: string postalAddress: string } > = { '<building-id-1>': { name: 'Example Campus Building A', postalAddress: '100 Main Street, Springfield, MA 01103', }, '<building-id-2>': { name: 'Example Campus Building B', postalAddress: '200 Main Street, Springfield, MA 01103', }, '<building-id-3>': { name: 'Example Campus Building C', postalAddress: '300 Main Street, Springfield, MA 01103', }, }
The rule looks up the imported building by Ref ID. If a matching entry exists, it applies the canonical name and postal address and selects the building.
Copy1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16export default function rules(entity: RulesEntity): RulesEntity { const building = BUILDINGS[entity.getRefId()] // Unknown buildings are left untouched so they appear in the // Grid for review instead of being assigned incorrect values. if (!building) { return entity } entity.setField('name', building.name) entity.setField('postalAddress', building.postalAddress) entity.setSelected(true) return entity }
Replace the example Ref IDs, building names, and addresses with values for your buildings.
Copy1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33const BUILDINGS = { 'building-ref-id-1': { name: 'Example Campus Building A', postalAddress: '100 Main Street, Springfield, MA 01103', }, 'building-ref-id-2': { name: 'Example Campus Building B', postalAddress: '200 Main Street, Springfield, MA 01103', }, 'building-ref-id-3': { name: 'Example Campus Building C', postalAddress: '300 Main Street, Springfield, MA 01103', }, } export default function rules(entity: RulesEntity): RulesEntity { const building = BUILDINGS[entity.getRefId()] // If this building is not in the lookup table, // leave it unchanged so it can be reviewed. if (!building) { return entity } entity.setField('name', building.name) entity.setField('postalAddress', building.postalAddress) entity.setSelected(true) return entity }
Do I have to use rules? No. They're optional advanced configuration. Connectors can be set up entirely by ticking and editing in the Grid.
Can a rule delete entities? A rule can untick a row (setSelected(false)), which un-selects an already-saved entity on the next Save. Items that are un-selected and then saved are intended to be removed from the Mapped Graph, but there is currently no capability to delete entities from the connector runtime. There's work for that to be a possibility in the future. In the meantime, the entity may become an orphan in the Graph.
What if the rule has an error? The editor shows it in the status bar and underlines it in the code. It can be saved so that you save your work.
What kind of errors can happen with rules? There are two different kinds of "error", and only one shows up in the Rules editor. First, the editor may complain (red underline, orange status bar), which indicates the rule doesn't quite typecheck. It still saves and still runs.
Second, the rule may crash while running, and this is indicated in the Grid, not the editor. In this case the rule does not apply. On every affected row, a red icon replaces the checkbox, and the row can't be selected or deselected until the rule is fixed. Hovering it says whether the rule failed on that one row or on every row.
Where do I see what fields I can use? The read-only types.d.ts tab in the Rules editor — it's generated per connector and always current.
Some connectors were originally configured with the older place-mappings model and are being moved onto Entities. That move is called Entities Migration. Migration is a one-time rename that switches a connector to the new Entities screens without rebuilding anything or losing any data, connections, or history.
Mapped is still rolling out the Connector Entities framework across connectors, so migration currently has limited availability.
If a connector has been configured before Entities existed, its data currently lives in the old place-mappings layout (a Site → Building → Floor → Space tree). To use the new Entities Grid and Merge Rules, that connector needs a one-time migration. Until it's done, the connector shows:
The new Entities screens stay hidden until the migration is complete. A connector already on Entities won't show any of this.
Migration DOES:
Migration DOES NOT:
Migration is done from the red Entities Migration section — it's a normal section, not a pop-up.
1 . Open the Entities Migration section. The old mapping table is still there and still works.
2 . Click Generate Report (top-right, where Import/Save normally sit). This previews the migration without changing anything. The report shows one tab per type:
3 . Review the report. This is the moment to check that the supported items look right and understand what's flagged as an orphan.
Click Migrate (top-right). This performs the rename, marks the items as saved, and switches the connector to Entities.
Afterwards: the old mapping table disappears, and the Entities screens appear — already populated with the migrated items, no re-import needed. If the connector remains in a STOPPED state, press Start to re-start polling. Run an import when you need fresh data from the source.
Nothing is deleted or rebuilt — it's a rename of existing items.
Generate Report changes nothing — they can preview as many times as they like before committing.
All connections and history are preserved. During migration, the connector stops briefly, during which period new sensor readings may be missed. Treat it like a short maintenance window. Migration should only take a few minutes, but the connector needs to be manually re-started afterwards by pressing the Start button.
Orphans are safe — they're recorded, not removed; a person can decide what to do with them later.
It's one-time — once migrated, they simply use the Entities screens from then on.