For AI agents: the documentation index is at /docs/aidbox/llms.txt. A Markdown version of this page is available at /docs/aidbox/configuration/migrations.md or by requesting it with the Accept: text/markdown header.
Aidbox Docs

Migrations

Aidbox supports one-time migrations that run exactly once during startup. Migrations are useful for:

  • Installing FHIR Implementation Guide packages
  • Running SQL statements (creating indexes, tables, seed data)

Migrations use the AidboxMigration resource combined with Init Bundle for declarative, idempotent execution.

How it works

  1. You define an AidboxMigration resource with an action and parameters
  2. You wrap it in an Init Bundle with ifNoneExist to ensure it runs only once
  3. On startup, Aidbox executes the bundle — if the migration already exists, it is skipped

Install a FHIR package

Use the far-migration-fhir-package-install action to install a FHIR IG package from the registry.

{
  "type": "transaction",
  "resourceType": "Bundle",
  "entry": [
    {
      "request": {
        "method": "POST",
        "url": "AidboxMigration",
        "ifNoneExist": "id=us-core-install"
      },
      "resource": {
        "resourceType": "AidboxMigration",
        "id": "us-core-install",
        "action": "far-migration-fhir-package-install",
        "status": "to-run",
        "params": {
          "resourceType": "Parameters",
          "parameter": [
            {
              "name": "package",
              "valueString": "hl7.fhir.us.core@3.1.1"
            }
          ]
        }
      }
    }
  ]
}

The package parameter value follows the format <package-name>@<version>.

After execution, the migration status changes to done and the result field contains the number of installed canonicals.

Uninstall a FHIR package

Use far-migration-fhir-package-uninstall to remove a previously installed package:

{
  "resourceType": "AidboxMigration",
  "id": "us-core-uninstall",
  "action": "far-migration-fhir-package-uninstall",
  "status": "to-run",
  "params": {
    "resourceType": "Parameters",
    "parameter": [
      {
        "name": "package",
        "valueString": "hl7.fhir.us.core@3.1.1"
      }
    ]
  }
}

Run a SQL migration

Use the aidbox-migration-run-sql action to execute arbitrary SQL statements.

Available since the 2602 release.

{
  "type": "transaction",
  "resourceType": "Bundle",
  "entry": [
    {
      "request": {
        "method": "POST",
        "url": "AidboxMigration",
        "ifNoneExist": "id=create-encounter-index"
      },
      "resource": {
        "resourceType": "AidboxMigration",
        "id": "create-encounter-index",
        "action": "aidbox-migration-run-sql",
        "status": "to-run",
        "params": {
          "resourceType": "Parameters",
          "parameter": [
            {
              "name": "sql",
              "valueString": "CREATE INDEX IF NOT EXISTS encounter_subject_id ON encounter ((resource #>> '{subject, id}'));"
            }
          ]
        }
      }
    }
  ]
}

After execution, the migration status changes to done and result.valueBoolean is true.

Invalid SQL causes the migration to fail with a 422 error. In a transaction bundle, this rolls back the entire transaction and prevents Aidbox from starting.

Run SQL outside a transaction

Available since the 2607 release.

By default Aidbox wraps the migration SQL in a transaction. PostgreSQL forbids some statements inside a transaction block, among them CREATE INDEX CONCURRENTLY, DROP INDEX CONCURRENTLY, REINDEX CONCURRENTLY, and VACUUM. Add the execution-type parameter with the value not-in-transaction to run the SQL with autocommit on:

{
  "resourceType": "AidboxMigration",
  "id": "create-encounter-index-concurrently",
  "action": "aidbox-migration-run-sql",
  "status": "to-run",
  "params": {
    "resourceType": "Parameters",
    "parameter": [
      {
        "name": "sql",
        "valueString": "CREATE INDEX CONCURRENTLY IF NOT EXISTS encounter_subject_id ON encounter ((resource #>> '{subject, id}'));"
      },
      {
        "name": "execution-type",
        "valueCode": "not-in-transaction"
      }
    ]
  }
}
execution-typeBehavior
in-transactionDefault, also applied when the parameter is absent. Aidbox runs the SQL inside a transaction and rolls it back on failure.
not-in-transactionAidbox runs the SQL with autocommit on, outside any transaction.

A migration that runs outside a transaction cannot be part of a FHIR transaction bundle, because the bundle itself is one atomic transaction. Post it in one of these ways instead:

How you send itexecution-type: not-in-transaction
POST /fhir/AidboxMigrationRuns outside a transaction
POST /AidboxMigration (Aidbox format)Runs outside a transaction
Entry in a batch bundleRuns outside a transaction
Entry in a transaction bundleRejected with a 422 error

Init Bundle examples on this page use "type": "transaction". To run a non-transactional migration on startup, set the bundle type to batch:

{
  "type": "batch",
  "resourceType": "Bundle",
  "entry": [
    {
      "request": {
        "method": "POST",
        "url": "AidboxMigration",
        "ifNoneExist": "id=create-encounter-index-concurrently"
      },
      "resource": {
        "resourceType": "AidboxMigration",
        "id": "create-encounter-index-concurrently",
        "action": "aidbox-migration-run-sql",
        "status": "to-run",
        "params": {
          "resourceType": "Parameters",
          "parameter": [
            {
              "name": "sql",
              "valueString": "CREATE INDEX CONCURRENTLY IF NOT EXISTS encounter_subject_id ON encounter ((resource #>> '{subject, id}'));"
            },
            {
              "name": "execution-type",
              "valueCode": "not-in-transaction"
            }
          ]
        }
      }
    }
  ]
}

Aidbox does not roll back a migration that runs outside a transaction. A failed CREATE INDEX CONCURRENTLY leaves an invalid index behind, and PostgreSQL does not use it for queries. Write the statement with IF NOT EXISTS, drop the invalid index, and run the migration again.

Find invalid indexes with:

SELECT c.relname
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
WHERE NOT i.indisvalid;

Using with Init Bundle

Set the BOX_INIT_BUNDLE environment variable to load migrations on startup:

volumes:
  - ./init-bundle.json:/tmp/init-bundle.json
environment:
  BOX_INIT_BUNDLE: file:///tmp/init-bundle.json

The ifNoneExist parameter in the bundle entry ensures idempotency — if a migration with the same id already exists, it is skipped. Without ifNoneExist, a repeated POST returns a 409 duplicate key error.

Combining multiple migrations

You can include multiple migrations in a single Init Bundle:

{
  "type": "transaction",
  "resourceType": "Bundle",
  "entry": [
    {
      "request": {
        "method": "POST",
        "url": "AidboxMigration",
        "ifNoneExist": "id=install-us-core"
      },
      "resource": {
        "resourceType": "AidboxMigration",
        "id": "install-us-core",
        "action": "far-migration-fhir-package-install",
        "status": "to-run",
        "params": {
          "resourceType": "Parameters",
          "parameter": [
            { "name": "package", "valueString": "hl7.fhir.us.core@3.1.1" }
          ]
        }
      }
    },
    {
      "request": {
        "method": "POST",
        "url": "AidboxMigration",
        "ifNoneExist": "id=create-custom-index"
      },
      "resource": {
        "resourceType": "AidboxMigration",
        "id": "create-custom-index",
        "action": "aidbox-migration-run-sql",
        "status": "to-run",
        "params": {
          "resourceType": "Parameters",
          "parameter": [
            {
              "name": "sql",
              "valueString": "CREATE INDEX IF NOT EXISTS patient_birthdate ON patient ((resource #>> '{birthDate}'));"
            }
          ]
        }
      }
    }
  ]
}

Checking migration status

List all migrations:

GET /fhir/AidboxMigration

Each migration has a status field:

StatusDescription
to-runMigration is queued for execution
doneMigration completed successfully

Comparison with POST /db/migrations

Aidbox also exposes a POST /db/migrations endpoint that accepts a plain [{id, sql}] array. The two approaches serve different use cases:

AidboxMigration + Init BundlePOST /db/migrations
When it runsAt Aidbox startup, before serving trafficAny time, called by an external client
External client requiredNoYes (needs credentials and a healthy Aidbox)
IdempotencyBuilt-in via ifNoneExistBuilt-in via migration id tracking
FHIR package installsYesNo
SQL outside a transactionYes, via execution-type in a batch bundle or a direct POSTNo, use $psql with Aidbox-Sql-Autocommit: true

Use AidboxMigration when you want zero-touch migrations on boot. Use POST /db/migrations when you need to apply migrations on demand from deployment scripts.

See also

Last updated: