Import / Export
The System → Import / Export page lets you move individual configuration artifacts between GEM installations as portable JSON bundles. Use it to share custom drivers, command sets, devices, zones, and other reusable building blocks without copying an entire database.
This is not a backup tool. For full-installation snapshots, see Backup & Restore.
A typical use: get a BACnet or Modbus zone configured exactly right on a staging system — with all of its object-instance numbers, register addresses, polling rates, and COV settings — then export the zone (with attributes) and import it on the production controller. No re-typing dozens of register addresses by hand.
Supported artifacts
| Type | References / children | Includes attributes |
|---|---|---|
Custom Drivers (user_driver) | self-contained | n/a |
Command Sets (command_set) | self-contained, carries child command rows | n/a |
Site Modes (site_mode) | self-contained | n/a |
Site Spaces (site_space) | _ref_site_mode, _ref_parent_site_space (self-ref) | n/a |
Sites (site) | _ref_site_mode | n/a |
Subsystems (subsystem) | optional _ref_device | n/a |
Devices (device) | command sets, response sets, device types, controllers | optional |
Zones (zone) | subsystems, devices, site spaces | optional |
AV Zones (av_zone) | composite zone ref {name, subsystem}, volume device, on/off macros | n/a |
UIs (ui) | _ref_site_space; carries child pages, widgets, and containers | n/a |
UI Widgets (ui_widget) | self-contained — exportable independently of any UI | n/a |
Macros (macro) | _ref_subsystem; carries child macro_step rows verbatim | n/a |
Triggers and themed UI components (zone groups, themes, control bars) still need richer remapping and arrive in a later phase.
Boilerplate templates
The full set above lets you ship a pre-built install template — exactly what's useful for restaurant chains, office chains, or repeated standard floor plans. The typical workflow on a fresh install:
- Import
site_mode,site_space, andsitefirst (so other artifacts can resolve_ref_site_*). - Import
subsystem,command_set,user_driver. - Import
deviceandzone(with attributes). - Import
av_zone(now that backing zones and volume devices exist). - Import
ui(now that site spaces exist). - Import
macro(last — anything else might be referenced inside step data).
The validate step catches out-of-order imports: any artifact whose refs don't resolve gets flagged in red, and the inline remap controls let you map a missing ref onto an existing row or auto-create a stub for subsystem / site_mode / site_space.
How references travel
Devices and zones point to other rows by foreign key — subsystem_id, command_set_id, device_type_id, etc. The export converts each FK to a natural-key reference by name, e.g.:
{ "type": "zone", "data": {
"name": "vav_103",
"_ref_subsystem": "climate",
"_ref_device": "ahu_main",
...
}}
On import, GEM looks up each _ref_* by name on the target installation. If a reference doesn't resolve, that artifact is flagged in the preview as Missing reference. You don't have to give up — the preview lets you remap each missing ref inline:
- Map to existing — pick the matching row from a dropdown of every row of that type on the target. Useful when the source named it
lightbut your target calls itlights. - Create new (subsystems only for now) — auto-create a stub on the target using the source's name. Other ref types must be created manually first or imported via their own bundle.
After picking actions, click Re-validate. The badges refresh and any artifacts whose refs now resolve flip to New or Already exists. Then click Import.
Naming-drift is the common case: people freely call the same subsystem lights, light, or Lights across installs. Map-to-existing handles all of these without renaming anything on either side.
AV Zones — composite zone reference
av_zone is a decoration on top of a regular zone (volume device, on/off macros, av_type). It has no name of its own and is uniquely identified by the (zone, volume_device) pair.
- The export bundles the av_zone with
_ref_zone: {name: "...", subsystem: "..."}(composite — the importer needs both pieces because zones are uniquely keyed by(name, subsystem_id)), plus_ref_volume_device,_ref_on_macro,_ref_off_macro. - On import, the backing zone must already exist on the target. There is no auto-create option — silently materializing a zone (with no driver/address/attributes) would be worse than failing loudly. The error message tells you exactly which zone+subsystem combination is missing so you can either import the parent zone first or create it manually.
- Conflict natural key:
(zone_id, volume_device_id). Overwrite replaces the av_zone row in place; rename doesn't apply (no name field) and falls back to skip.
Standalone UI widgets
Sometimes you don't want the whole UI template — just a useful widget (a custom dashboard tile, a curated scene picker, etc.) to slot into your own UI on the target. Pick UI Widgets in the export panel and tick the ones you want.
On import:
- The widget is matched on the target by
name. If the same name exists, the standard conflict resolution (skip / overwrite / rename) applies. - The imported widget is not placed on any UI — you assign it manually through the UI editor afterwards.
- This composes cleanly with full UI imports: if you've already imported a widget standalone and then import a UI that references it by name, the UI re-links to the existing widget rather than creating a duplicate.
UIs — pages, widgets, and containers
UI exports are a tree, not a single row. Each UI artifact carries:
- The
uirow itself (label, page_groups JSON, optional_ref_site_space). - Every page assigned to the UI (
ui_pagerows linked throughui_page_ui). - Every widget placement on those pages (
ui_page_widgetrows) including the placement-levelconfigblob, sort_index, and title. - The widget instance (
ui_widgetrow) and its container (ui_widget_container), if any.
On import:
- Pages are matched to existing rows by
nameto avoid duplicates when re-importing — if a page with that name already exists, the import re-links to it rather than creating a copy. - Widgets and containers behave the same way (matched by name).
- On
overwrite, the existing UI's page links and widget placements are wiped and re-created — the underlyingui_page/ui_widget/ui_widget_containerrows themselves are intentionally not deleted because they may be shared with other UIs.
Caveat — internal references inside widget config aren't remapped. Widget configs that point at specific zones, devices, attributes, or macros (e.g. a "Lights" toggle widget bound to
zone_id: 42) will carry those IDs verbatim. If the IDs don't line up on the target, the widget will fail at runtime. This is the same passthrough caveat asmacro_step.dataand is part of the deeper Phase-3 step-data remap work.
Macros and step-type compatibility
Macros export with their full ordered step list. The bundle records the GEM version the export came from (exported_from_version).
Before importing a macro, GEM checks that every step driver (command, if_attribute, webpush_notification, etc.) is registered in the target install's macro-step driver registry. If any step type is unknown — typically because the target is running an older GEM version that doesn't have that step yet — the import is refused with a message naming the unknown step types and the source bundle's version, e.g.:
unknown macro step type(s): call_notification. Bundle was exported from GEM v1.4.0. Update this install (or remove the unsupported steps from the bundle) before importing.
The preview surfaces this as Unsupported step type before you click Import, so you can update the target first or trim the bundle.
Caveat — internal references inside step.data are not remapped. A
commandstep'sdevice_id/zone_id, aset_attributestep's attribute name, arun_macrostep's target, etc. are exported as-is. If those IDs don't line up on the target install, the macro will fail at runtime. A general step-data remap tool is the next phase of this work.
Attributes
For devices and zones, tick Include attributes in the export panel to bundle every attribute attached to the artifact (BACnet object IDs, Modbus registers, polling intervals, names, labels, value options, etc.). On import, attributes are written against the new device/zone with these rules:
- Skipped on export:
secureattributes — they're encrypted with the source installation's key and can't be decrypted on the target. Re-enter secrets (passwords, API keys, etc.) manually after import. - Included on export: everything else, including
readonlyattributes —readonlymeans "locked from UI editing", not "runtime state", so it's exactly the BACnet object number / Modbus register kind of config you want to ship. - Conflict mode =
overwrite: existing attributes with matching names get their values replaced; unrelated attributes on the target are untouched. - Conflict mode =
skip/rename: new artifact gets the bundled attributes; existing artifacts are left alone (skip) or duplicated under a new name (rename).
Bundle format
Bundles are plain JSON. The current version is 1.
{
"gem_export_version": 1,
"exported_at": "2026-04-25T18:00:00.000Z",
"artifacts": [
{
"type": "user_driver",
"data": {
"name": "acme_thermostat",
"code": "...",
"system_target": "device",
"base_driver": "generic_tcp",
"enabled": true
}
},
{
"type": "command_set",
"data": {
"name": "Lutron RadioRA",
"description": "...",
"commands": [
{ "id": 1, "name": "On", "template": "...", "args": null, "arg_options": null }
]
}
},
{
"type": "zone",
"data": {
"name": "vav_103",
"label": "VAV 103",
"_ref_subsystem": "climate",
"_ref_device": "ahu_main",
"_ref_site_space": null,
"_attributes": [
{ "name": "object_instance", "value": "23", "value_type": "integer", "readonly": true },
{ "name": "polling_rate", "value": "5", "value_type": "integer", "readonly": false }
]
}
}
]
}
Database id columns are stripped on export — the target installation assigns its own. For command sets, the per-set command.id is preserved so any internal references inside the set keep working. For devices and zones, every FK column is replaced with a _ref_<col> natural-key entry that the importer resolves by name lookup.
Exporting
- Open System → Import / Export.
- Tick the artifacts you want in each section.
- Click Download Bundle. The browser downloads a
gem_export_<timestamp>.jsonfile.
Importing
- Click Choose File and pick a bundle.
- The page previews each artifact and tells you whether it's New, Already exists, or Missing reference (a referenced subsystem/device/etc. wasn't found on the target). Natural keys:
name+system_targetfor drivers,namefor command sets and devices,name+subsystemfor zones. - For each conflict, choose:
- Skip (default) — leave the existing artifact alone.
- Overwrite existing — replace the existing row's contents. For command sets, child commands are deleted and re-created.
- Import as new (renamed) — create a new row with
" (imported)"appended to the name (or" (imported 2)",(imported 3)etc. if needed).
- Click Import. The result table shows what was created, updated, skipped, or errored.
Security warning
Imported custom driver code executes on the target installation when enabled. Only import bundles from sources you trust.
The same caution applies to any future artifact type that contains executable JavaScript (macros with run_script steps, scripted triggers, etc.). When those types are added, imports will require an explicit per-artifact "trust this code" gate before they activate.