Skip to main content

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

TypeReferences / childrenIncludes attributes
Custom Drivers (user_driver)self-containedn/a
Command Sets (command_set)self-contained, carries child command rowsn/a
Site Modes (site_mode)self-containedn/a
Site Spaces (site_space)_ref_site_mode, _ref_parent_site_space (self-ref)n/a
Sites (site)_ref_site_moden/a
Subsystems (subsystem)optional _ref_devicen/a
Devices (device)command sets, response sets, device types, controllersoptional
Zones (zone)subsystems, devices, site spacesoptional
AV Zones (av_zone)composite zone ref {name, subsystem}, volume device, on/off macrosn/a
UIs (ui)_ref_site_space; carries child pages, widgets, and containersn/a
UI Widgets (ui_widget)self-contained — exportable independently of any UIn/a
Macros (macro)_ref_subsystem; carries child macro_step rows verbatimn/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:

  1. Import site_mode, site_space, and site first (so other artifacts can resolve _ref_site_*).
  2. Import subsystem, command_set, user_driver.
  3. Import device and zone (with attributes).
  4. Import av_zone (now that backing zones and volume devices exist).
  5. Import ui (now that site spaces exist).
  6. 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 light but your target calls it lights.
  • 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 ui row itself (label, page_groups JSON, optional _ref_site_space).
  • Every page assigned to the UI (ui_page rows linked through ui_page_ui).
  • Every widget placement on those pages (ui_page_widget rows) including the placement-level config blob, sort_index, and title.
  • The widget instance (ui_widget row) and its container (ui_widget_container), if any.

On import:

  • Pages are matched to existing rows by name to 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 underlying ui_page / ui_widget / ui_widget_container rows 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 as macro_step.data and 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 command step's device_id/zone_id, a set_attribute step's attribute name, a run_macro step'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: secure attributes — 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 readonly attributes — readonly means "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

  1. Open System → Import / Export.
  2. Tick the artifacts you want in each section.
  3. Click Download Bundle. The browser downloads a gem_export_<timestamp>.json file.

Importing

  1. Click Choose File and pick a bundle.
  2. 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_target for drivers, name for command sets and devices, name + subsystem for zones.
  3. 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).
  4. 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.