Configuration Exchange Format
Configuration Exchange moves a platform's configuration as one YAML bundle: Settings → Configuration Exchange exports the active organization's resources together with the platform organization directory, and imports a bundle back with a dry-run plan first. Resources upsert by their natural name, so re-importing the same bundle is idempotent. The configuration exchange guide covers the workflow; this page is the format itself.
What an export carries - and what it cannot
An export contains every configurable resource of the platform: the organization directory (organizations, manual memberships, IdP mappings), secret backends, sites, devices, git repositories, inventory providers, variables, notification destinations, flows with their attachments and defaults, schedules, webhooks, flow subscriptions, file repositories, route and action permission overrides, and platform sync profiles. A meta-test holds this list complete: every database model is either exportable or excluded for one of the reasons below, so a new resource type cannot ship outside the format.
Deliberately not in a bundle:
- Secret values - the
secretssection is import-only: an import writes values into the configured backend, an export never reads them back. Bundles reference secrets through*_reftemplate strings instead. - Webhook secrets and path tokens - regenerated on import; rotate and redistribute the trigger URLs afterwards.
- Users and API tokens - users are provisioned by the identity provider, and tokens store only a salted digest, so both must be re-created against the new instance.
- Run history, audit logs, sync history - records of what happened, not configuration.
- File-store contents - a file repository entry carries the connection (endpoint, bucket, prefix); the stored objects move at the storage level.
- Environment-level settings - deployment configuration lives in environment variables, outside the database and outside the bundle.
Permission overrides travel in two sections mirroring the two tabs of Settings → Permissions: permission_action_overrides reallocates a whole resource:verb action to a policy (keyed by the action), and permission_overrides pins a single method and path (keyed by both). An import rejects an action or route the registry does not know and a policy the permissions reference marks as not overridable for it, so a bundle cannot grant what the editor would refuse.
A flow's device-target fields carry their default devices by name, not by id, so the defaults survive a move to another instance. An import turns each name into the id of a device in the target organization. A name that matches no device is left out of the default, and the import result lists it as a warning naming the flow, the field, and the device. Import the devices first (or sync the inventory provider that brings them in), or import the flow again once they exist. Instance bootstrap does this second pass for you after its last provider sync.
The template
This is the schema template the Load Template button loads, generated from the import contract - section order is the importer's apply order, and every accepted field appears (fields the examples do not show are listed as commented stubs):
# Hegemony Import/Export Schema (v2)
# ==========================================
# Import order: secret_backends → secrets → file_repositories →
# registry_credentials → sites → devices → git_repositories →
# inventory_providers → variables → notification_destinations → flows →
# schedules → webhooks → flow_subscriptions → permission_overrides →
# permission_action_overrides → platform_sync_profiles
# Upsert by 'name' field - existing items are updated, new items are created.
schema_version: 2
# exported_at is set automatically on export (ISO-8601 UTC timestamp)
# Organization the resource sections below belong to (slug). Exports stamp the
# active organization; imports apply every resource into this organization
# (creating it first from the organizations directory below if needed).
# Omit for pre-tenancy bundles: they import into the caller's active org.
organization: default
# Platform organization directory: organizations with their memberships and
# IdP mappings. Upserted by slug before any resources are applied. Members
# reference users by IdP subject; unknown subjects are skipped with a warning
# (users are provisioned by the identity provider, never by an import).
organizations:
- slug: default # Immutable identifier (required)
name: Default # Display name (required)
description: "" # Optional
is_active: true
is_shared: false # Designated shared org (at most one entry)
members:
- user: 6f1e...-keycloak-subject
username: alice # Informational only
role: admin # admin | operator | approver | auditor | viewer
source: manual # manual | idp
idp_mappings:
- claim: groups # Must match HEGEMONY_ORG_IDP_GROUP_CLAIM
value: /orgs/default
role: viewer
is_active: true
# Secret backends the secrets below resolve through. Upserted by 'tag'.
# 'config' uses *_ref template strings, never raw secret values, so backends
# round-trip safely.
secret_backends:
- tag: vault_internal # Unique identifier referenced by secrets.backend_ref
scheme: vault # URI scheme secret refs use ("vault://...")
name: Internal OpenBao # Display name (optional)
type: openbao # Backend type from the secret-backend registry
schema_type: vault_approle # Schema variant; the bundled backend requires this
is_default: true # At most one entry; used when a secret names no backend
enabled: true
read_only: false # true blocks writes, for a backend you only read from
description: Bundled OpenBao instance
config: # Type-specific settings, refs only
address: https://openbao:8200
ca_cert_file: /openbao/tls/ca.crt
- tag: vault_external # A second backend, addressed by its own scheme
scheme: vault_external # ...so its refs read "vault_external://..."
name: External Vault
type: vault # OpenBao and HashiCorp Vault are separate types
is_default: false
config:
address: https://vault.example.com:8200
token_ref: "{{ env('VAULT_TOKEN') }}"
# Secrets are IMPORT-ONLY and are never exported.
# Values are written to the configured secrets backend and only key names are
# visible elsewhere in the app. Use backend_ref/backend_scheme/backend_name to
# select a backend, or omit them to use the default backend.
secrets:
- name: shoutrrr-url # Unique identifier (required)
description: Shoutrrr notification URL (referenced by ops-shoutrrr below)
backend_ref: vault_internal # Optional backend tag
folder: orgs/default/secrets/shoutrrr
values: # Required, supports one or more keys
url: discord://example-token@123456789
- name: discord-webhook
description: Discord bot token (referenced by ops-discord below)
# backend_scheme/backend_name select a backend instead of backend_ref, for
# bundles written before backends had tags. backend_ref wins if both appear.
backend_scheme: vault
backend_name: vault_internal
folder: orgs/default/secrets/discord
values:
token: example-discord-bot-token
- name: github-webhook-hmac
description: Inbound webhook credential (referenced by github-deploy below)
folder: orgs/default/secrets/github-webhook
values:
# An inbound webhook reads the first of value/secret/hmac/hmac_key/token/key
# found in its secret's folder.
secret: example-hmac-signing-key
# File repositories: the shared file store flows read firmware, scripts, and
# other assets from. Credential values are never part of this format - config
# carries template refs that resolve at use time - and neither are the stored
# files themselves: reattach the repository to the same bucket, or copy the
# objects at the storage level.
file_repositories:
- name: artifacts # Unique identifier (required)
kind: s3 # Storage kind
endpoint_url: https://s3.example.invalid
bucket: hegemony-artifacts
prefix: shared/ # Optional key prefix within the bucket
config: # Kind-specific connection settings; omit to use
# the deployment's own object storage
access_key_id_ref: "{{ secret('vault://orgs/acme/secrets/s3/access-key') }}"
secret_access_key_ref: "{{ secret('vault://orgs/acme/secrets/s3/secret-key') }}"
region: us-east-1
is_default: true # At most one entry; the browser opens this one
files: # Import-only: catalogs objects already seeded into
# the bucket so they show up in the file browser.
# Export never emits this.
- filename: baseline-config.txt
object_key: shared/golden/baseline-config.txt
sha256: "3b1f2c9d4e5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e"
size_bytes: 512
folder_path: golden # Optional folder the entry appears under
# Registry credentials: logins for external container registries. Container
# steps in the organization use the credential whose host matches their image
# automatically. The password is a secret reference, never a value.
registry_credentials:
- host: ghcr.io # Registry host as written in image references
# (with an optional :port; docker.io for Docker Hub)
username: robot-account
password_ref: "{{ secret('vault://orgs/acme/secrets/ghcr/token') }}"
description: Pulls the team's private runner images
# Sites form a hierarchy (use path for unique identification)
# Path format: "Parent/Child/GrandChild" - e.g., "Global/Europe/Prague"
# Only local sites travel in a bundle. Sites synced from an inventory provider
# are never exported, a bundle path that lands on one fails with
# OPERATOR_ACTION_REQUIRED, and a local site cannot name one as its parent.
sites:
- name: Global # Unique identifier (required)
description: Global root # Optional description
location: "" # Optional location string
tags: # Optional key-value tags
type: root
- name: Europe
parent: Global # Parent path (just "Global" for root children)
- name: Prague
description: Prague datacenter
location: V Parku 4, Praha 4
tags:
region: emea
env: prod
parent: Global/Europe # Full path to parent site
# Devices belong to sites and use access_config references
devices:
- name: router-1 # Unique identifier (required)
hostname: router-1.lab # DNS hostname (optional)
mgmt_host: 192.168.1.1 # Management IP (required if no hostname)
mgmt_port: 22 # SSH port (default: 22)
mgmt_ips: [192.168.1.2] # Additional management addresses (optional)
attributes: # Typed inventory facts, independent of connection settings
vendor: Cisco
model: CSR1000v
current_version: 17.3.4a
asset_tag: EDGE-001
critical: true
service_priority: 1
custodians: [Network Operations]
site: Global/Europe/Prague # Full path to site
access_config: # Secret refs only: env()/file() are platform configuration
ssh:
username_ref: "{{ secret('vault://orgs/default/secrets/network/ssh/username') }}"
password_ref: "{{ secret('vault://orgs/default/secrets/network/ssh/password') }}"
enable:
password_ref: "{{ secret('vault://orgs/default/secrets/network/enable/password') }}"
platform: ios-xe # Platform: ios-xe (default)
tags: # Optional key-value tags
role: edge
# Git repository connections for flow export/import from Git
# Secret references are resolved at runtime by workers
git_repositories:
- name: infra-repo # Unique identifier (required)
url: https://github.com/org/infra-configs.git # Clone URL (required)
branch: main # Default branch (default: main)
auth_secret_ref: "{{ secret('vault://orgs/default/secrets/git/infra/token') }}" # HTTPS auth token secret ref
# ssh_key_secret_ref: "{{ secret('vault://orgs/default/secrets/git/infra/ssh_key') }}" # SSH key secret ref (mutually exclusive with auth_secret_ref)
sync_stale_seconds: 300 # Cache stale threshold (default: 300, 0 = always sync)
# allow_insecure_url: true # Per-repo opt-in for http:///private remotes (disables the
# # SSRF guard for THIS repo only; e.g. an internal demo Gitea)
# External inventory providers. The provider type comes from an installed inventory
# provider plugin; GET /inventory/provider-types lists what this instance has.
# netbox and git are the first-party plugins.
# 'config' uses *_ref template strings (env / vault refs), never raw secrets.
# Imported providers can be temporarily disabled while operators verify
# credentials and connectivity.
inventory_providers:
- name: netbox-prod # Slug ([a-z0-9][a-z0-9_-]*)
provider_type: netbox # A registered provider type (see /inventory/provider-types)
description: Production NetBox # Optional
enabled: false # Default false on first import; flip on after testing
sync_interval_seconds: 1800 # Automatic sync cadence; 0 = manual sync only (default 1800)
delta_sync_enabled: false # Use incremental reads when the provider supports them
full_reconcile_interval_seconds: 86400 # Full reconciliation cadence; 0 = every sync
# object_types: [ip_prefix, vlan] # Types beyond devices/sites to sync; omit = every type the plugin serves
config:
url: https://netbox.example.invalid
token_ref: "{{ env('NETBOX_TOKEN') }}"
- name: git-inventory # Slug ([a-z0-9][a-z0-9_-]*)
provider_type: git # A registered provider type (see /inventory/provider-types)
enabled: false # Default false on first import; flip on after testing
config:
# Portable name reference to a git_repositories entry above; resolved to the
# target instance's repository UUID on import (never export the raw git_repo_id).
git_repository: infra-repo
branch: main
path: inventory
# Global variables resolved via {{ vars.NAME }}
variables:
- name: DEFAULT_TIMEOUT # Variable name (letters, digits, underscore)
value: 300 # Supports string/int/bool/float/list/dict
description: Default timeout for operations
# Notification destinations define where to send alerts.
# 'type' is a base transport (email, shoutrrr, outgoing_webhook) or any
# installed preset destination type (e.g. discord, slack, teams_power_automate);
# preset types carry their own friendly config fields and are validated against
# the notification registry on import.
notification_destinations:
- name: ops-shoutrrr # Raw base transport (arbitrary Shoutrrr URL)
type: shoutrrr
enabled: true # Whether destination is active (default: true)
config_json: # Type-specific configuration (required)
url_secret: "{{ secret('vault://orgs/default/secrets/shoutrrr/url') }}"
title: Hegemony
- name: ops-discord # Preset destination type (friendly fields)
type: discord
enabled: true
config_json:
token: "{{ secret('vault://orgs/default/secrets/discord/token') }}"
webhook_id: "123456789"
# Flows define automation workflows
flows:
- name: Simple Check
description: Basic connectivity check flow
tags: # Optional key-value tags; the flow list groups by them
team: netops
risk: low
pinned: true # Pin to the top of the flow list for everyone
manual_runs_enabled: false # Automation-only: a schedule, a webhook or another
# flow's Run Flow step may start it; nobody by
# hand, except an admin forcing one audited run
definition:
schema_version: 1
interface_graph:
nodes:
- id: interface
kind: field
position_x: 80
position_y: 80
data:
field_id: interface
field_type: core.text
label: Interface
required: true
default_value: GigabitEthernet0/1
description: Interface to check
- id: device
kind: field
position_x: 320
position_y: 80
data:
field_id: device
field_type: inventory.device
label: Primary target device
required: true
config:
role: default
multiple: false
option_source:
kind: inventory.devices
edges: []
# Optional Git link. Naming a git_repositories entry above puts the flow
# under version control; sync_mode 'readonly' also makes it uneditable in
# the UI.
git_repository: infra-repo
git_path: flows/simple-check.yaml
git_branch: main
sync_mode: synced # synced | readonly | detached
auto_commit: true # Push each commit of the flow automatically
# Notification subscriptions can be written inline here instead of in the
# flow_subscriptions section below; both forms are accepted.
notifications:
- destination: ops-discord
event: run.failed
enabled: true
filters_json: {}
# Root-level defaults merged into every flow's nodes and edges on import, so
# the definitions above only need to carry what differs. These are the
# platform's current defaults, which is exactly what an export emits; an
# explicit field on a node always wins.
# Omit the section to use the platform defaults.
flow_defaults:
node:
type: step
tags: {}
handler: null
policy: null
mounted_files: null
trigger_limit: null
join_mode: null
failure_policy: null
cancel_policy: null
branch_mode: null
cases: null
value_expr: null
loop_mode: null
condition: null
count: null
items_expr: null
max_iterations: null
iteration_delay_sec: null
collect: null
on_body_failure: null
max_parallel_iterations: null
parallel_failure_policy: null
loop_ref: null
assignments: null
terminal_status: null
scope_id: null
position_x: 0
position_y: 0
target_selectors: []
params: {}
config: {}
edge:
type: execution
label: null
waypoints: null
interface_node:
parent_id: null
width: null
height: null
position_x: 0
position_y: 0
interface_node_data:
label: null
description: null
field_id: null
field_type: null
required: false
default_value: null
config: {}
runtime_layout: null
collapsible: false
interface_edge:
relations: []
label: null
# Flow attachments (scripts, templates, configs)
flow_attachments:
- flow: Simple Check # Flow name reference
filename: scripts/check.py # Filename (path-like ok)
content: | # File content (text)
#!/usr/bin/env python3
print("hello")
# Where this file lives when the flow is Git-linked
git_repository: infra-repo
git_file_path: flows/simple-check/scripts/check.py
# Schedules trigger flow runs on a schedule
# Note: schedules are imported as DISABLED for safety
schedules:
- name: nightly-check # Unique identifier (required)
flow: Simple Check # Flow name reference (required)
schedule_type: recurring # one_time or recurring (required)
cron_expression: "0 2 * * *" # Cron for recurring
# interval_seconds: 3600 # Alternative to cron for recurring (minimum 60)
# run_at: 2026-01-01T02:00:00Z # Required for schedule_type: one_time
timezone: UTC # IANA timezone (default: UTC)
related_targets: # Device targets per flow role, by device name
default:
- router-1
params: {} # Optional run parameters
enabled: true # Whether schedule is active
# Webhook endpoints for inbound triggers
# Auth material travels as secret_ref (a secret NAME); secret values and path
# tokens are never exported. The first import assigns a path token; later
# imports keep it, so the endpoint URL survives updates.
webhooks:
- name: github-deploy # Unique identifier (required)
description: GitHub deploy webhook
flow: Simple Check # Flow name reference (required)
auth_mode: hmac_sha256 # hmac_sha256 or bearer
secret_ref: github-webhook-hmac # Secret holding the signing key / bearer token
replay_window_seconds: 300
rate_limit_per_minute: 60
# flow_version: 3 # Pin to one committed version; omit to use the latest
allowed_source_ips: # Optional allowlist (CIDR or plain address)
- 203.0.113.0/24
payload_mapping: {} # Optional map from request payload to flow parameters
enabled: true
# Flow notification subscriptions bind destinations to flow events
flow_subscriptions:
- flow: Simple Check # Flow name reference (required)
destination: ops-discord # Destination name reference (required)
event: run.failed # Event type (see the notification event vocabulary)
enabled: true # Whether subscription is active (default: true)
filters_json: {} # Optional event filters
# Route permission overrides: pin one method and path to a policy, overriding
# the route's action (and any action-level override below).
permission_overrides:
- method: POST
path: /runs
policy: operator # A policy from actions.yaml, or 'disabled' to switch it off
# Action permission overrides: reallocate a whole resource:verb action to a
# policy; every route of the action follows unless a route override above
# pins it. Structural (public/internal/custom) actions cannot be overridden,
# and 'disabled' switches an action off for every caller until another policy
# is assigned (the actions the console needs to recover refuse it).
permission_action_overrides:
- action: run:trigger # An action key from apps/api/auth/actions.yaml
policy: admin # A policy from actions.yaml, or 'disabled'
# Platform sync profiles: the profiles that keep this platform's configuration
# synchronized with a Git repository. Exporting them is what lets a restored
# instance keep syncing without being reconfigured by hand. They are also part
# of Platform Sync's default "all families" backup; add platform_sync_profile
# to excluded_families when a particular profile should omit these definitions.
platform_sync_profiles:
- name: prod-backup # Unique identifier (required)
description: Nightly configuration backup
mode: backup # backup | mirror | bidirectional
git_repository: infra-repo # Name reference to a git_repositories entry above
branch: main
base_path: platform/prod # Directory within the repository
enabled: true
schedule_enabled: true
schedule_interval_seconds: 3600 # Required when schedule_enabled is true
included_families: # Empty means every supported family
- flow
- site
- device
excluded_families: [] # Must not overlap included_families
destructive_policy: block # block | allow_with_confirmation | allow_automated
auto_apply_non_destructive: false # Not valid in backup mode