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.
Export, preview, and import all require an elevated role. An import writes custom driver code that is compiled and run at device load, and an export dumps device and zone configuration wholesale — neither is reachable from a non-elevated session even with a wildcard API grant. See Roles.
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, device types, controllers; carries the device's command/response script | 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, containers, controls, zones, and macro buttons | n/a |
UI Pages (ui_page) | carries its widget placements — exportable independently of any UI | n/a |
UI Widgets (ui_widget) | self-contained — exportable independently of any UI | n/a |
UI Themes (ui_theme) | self-contained | n/a |
Macros (macro) | _ref_subsystem; carries child macro_step rows verbatim | n/a |
Schedules (macro_schedule) | _ref_macro | n/a |
Triggers (attribute_trigger) | self-contained | n/a |
Variables (variable) | self-contained — identified by prefix, not name | n/a |
Alarms (alarm_definition) | self-contained | n/a |
Monitors (monitor) | device, composite zone ref {name, subsystem}, self-ref dependency | n/a |
Monitor Tags (monitor_tag) | anchor monitor, up/down macros | n/a |
Reports (report_definition) | carries child report_schedule rows | n/a |
Notification Profiles (notification_profile) | self-contained | n/a |
Holiday Calendars (holiday_calendar) | self-contained | n/a |
Web Services (web_service) | self-contained — inbound credentials stripped, see What a bundle never carries | n/a |
Channel Providers (channel_provider) | carries child channel rows | n/a |
Device Types (device_type) | carries child device_subtype rows | n/a |
AV Sources (av_source) | device, on/off macros, channel provider — identified by label | n/a |
Runtime state is deliberately left behind. A monitor's current state, a schedule's last run, a trigger's last fire and a report's last result are facts about the source system, not configuration, so they are stripped on export rather than carried over to make the target briefly lie about its own history.
Zone references travel as a composite {name, subsystem} rather than a bare
name, because zone names are only unique within a subsystem — a bare name could
otherwise bind to a zone in an entirely different subsystem without anything
looking wrong.
An AV Source's av_zone_id is dropped on export: an AV zone has no name of its
own (it is identified by its backing zone plus volume device), so there is
nothing stable to reference it by. Re-bind it on the target after import.
A device's command and response scripts travel with it — a device without them isn't the same device. They are JavaScript that runs on the target when the device is loaded, so the security warning below applies to device bundles, not only to custom drivers.
To ship these as one versioned, installable, removable unit — rather than a one-way bundle — see Applications.
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).
What a bundle never carries
Two classes of field are dropped so that a bundle is safe to hand to another integrator, attach to a ticket, or keep in a repository.
Web service credentials. A web service's username,
password, api_key and bearer_token are the inbound shared secrets external callers
authenticate against. Unlike attributes they have no encryption layer, so they would appear in the
bundle in plain text. The web_service row still imports — re-enter its secret on the target
install and the endpoint works.
Secure attributes are skipped for the separate reason described above: they're encrypted with the source installation's key and cannot be decrypted on the target.
What an import cannot change
A bundle may create a macro or a device that declares itself Elevated, because that is a
package describing what it ships. It may not clear that flag on a record that already exists
here: on the overwrite path the is_elevated column is dropped from the update.
That flag is the per-record authorization boundary — it is what keeps "Disarm Alarm", "Unlock All Doors" and the lock devices themselves off non-admin UIs and PIN panels (see Roles). An import that could quietly demote one would be a way to open those to every panel in the building without touching a single role.
Anonymous web-service access is never imported, on any path — new row, overwrite or rename. A
web service is served with no login session, so one that arrived
already marked anonymous would be a world-callable endpoint running a script that holds the live
server instance. An imported service lands unpublished and is refused until an administrator opens
it here and chooses None deliberately. The same applies to a .gemapp
application, whose acknowledgement asks about running third-party code, not
about publishing an unauthenticated endpoint.
An overwrite additionally leaves an existing service's auth type alone — a bundle may declare the auth type of a service it creates, but it may not restyle the authentication of one already running here.
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 an Already exists artifact, the name is a link that opens the existing record it collides with — in a modal, so the loaded bundle and your conflict choices stay put. (Types the modal can't render stay plain text.) - 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. Names in the result table link to the imported (or pre-existing) record the same way, opening it in a modal rather than navigating away from the results.
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 every other artifact that carries executable JavaScript — a device's command/response scripts, a web service, and a macro with run_script steps. Import applies no code gate: it writes what the bundle contains.
Applications install the same artifacts through a package that is signed, inspected before anything is written, and gated on publisher trust — code from an untrusted publisher requires explicit acknowledgement, or is refused outright when the install is set to accept trusted publishers only.