Skip to content

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 secrets section is import-only: an import writes values into the configured backend, an export never reads them back. Bundles reference secrets through *_ref template 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):

yaml
# 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

Released as open source under the AGPL-3.0-or-later license. Development is sponsored by Rexonix s.r.o.. Contact — [email protected].