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.

Elevated access required

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

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, device types, controllers; carries the device's command/response scriptoptional
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, containers, controls, zones, and macro buttonsn/a
UI Pages (ui_page)carries its widget placements — exportable independently of any UIn/a
UI Widgets (ui_widget)self-contained — exportable independently of any UIn/a
UI Themes (ui_theme)self-containedn/a
Macros (macro)_ref_subsystem; carries child macro_step rows verbatimn/a
Schedules (macro_schedule)_ref_macron/a
Triggers (attribute_trigger)self-containedn/a
Variables (variable)self-contained — identified by prefix, not namen/a
Alarms (alarm_definition)self-containedn/a
Monitors (monitor)device, composite zone ref {name, subsystem}, self-ref dependencyn/a
Monitor Tags (monitor_tag)anchor monitor, up/down macrosn/a
Reports (report_definition)carries child report_schedule rowsn/a
Notification Profiles (notification_profile)self-containedn/a
Holiday Calendars (holiday_calendar)self-containedn/a
Web Services (web_service)self-contained — inbound credentials stripped, see What a bundle never carriesn/a
Channel Providers (channel_provider)carries child channel rowsn/a
Device Types (device_type)carries child device_subtype rowsn/a
AV Sources (av_source)device, on/off macros, channel provider — identified by labeln/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.

Packaging a whole application

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:

  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).

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

  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. 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.)
  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. 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.

A gated alternative

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.