Skip to content

Migrate from OpenMed 3.0 to 3.1

Move production effects behind an application-owned permission and review boundary. Keep clinical values and credentials inside trusted local code; carry controlled outcomes, counts, opaque references and digests into review and audit artifacts. OpenMed does not make autonomous clinical decisions.

This guide describes contracts already on master. Their presence does not install an end-to-end governed executor, and preparing v3.1 does not mean a v3.1 package or container is published. Verify imports against the exact source revision or released package you deploy. Implementation PRs extend these sections when their APIs land.

Direct-path inventory

Existing path Actual effect Migration boundary available on master
FHIRServerClient.put_resource(resource, write=True) Sends an HTTP PUT; write=False sends no request Legacy, outside complete v3.1 governance. Use signed authority, a purpose ticket, minimum-data planning, cached capability preflight, reviewed approval and an application-owned bounded FHIR executor
FHIRServerClient.fetch_and_deidentify(..., write=True) Reads and transforms resources, then sends PUTs Legacy, outside complete v3.1 governance. Separate authorized intake and transformation from individually reviewed proposed writes; a fetch is still network access when write=False
OpenMRSAdapter.write_back(records) Delegates REST/FHIR2 writes; dry_run defaults to False Legacy, outside complete v3.1 governance. Explicitly use dry_run=True for planning and supply the complete application boundary before live dispatch
load_grounded_notes plus write_omop_duckdb Builds rows in memory, then the writer creates/updates a database The loader does not write. The writer remains legacy; explicitly stage intended mutations, reference checks, vocabulary provenance, rollback coverage and approval before an application-owned atomic committer
load_grounded_notes plus write_omop_sqlite Builds rows in memory, then the writer commits database changes The writer remains legacy. Explicitly compose SQLiteOmopBatchCommitter with protected rollback custody, vocabulary checks and independent host authorization of staging and database previews; selecting the legacy writer does not activate these guards
load_grounded_notes plus write_omop_parquet Builds rows in memory, then persists per-table files Legacy file persistence. No governed Parquet replacement is provided here; retain an explicit application policy/review boundary rather than treating file export as a transactional OMOP commit
to_bundle(..., bundle_type="transaction") Constructs an in-memory export with per-entry request metadata Artifact assembly, not transaction dispatch. Keep the payload protected; a separate bounded executor must verify authority and approval before sending it

No wrapper on master automatically converts these legacy clients or writer functions into a complete governed workflow. The generic staged OMOP API accepts an explicit committer. For the eleven loader-owned SQLite tables, SQLiteOmopBatchCommitter provides a bounded transactional implementation when the host supplies trusted authorization, admission, connection and rollback-custody integrations. It requires independent review of the complete staging packet and database outer preview, with the exact inner batch binding used for submission. Mutations and the value-free receipt commit in one BEGIN IMMEDIATE transaction after fresh row-state, schema, reference and vocabulary checks. Recovery reads the exact durable receipt without replay; an unknown outcome requires reconciliation. Protect database, receipt and rollback custody together: restoring or rewriting the database and receipt ledger together defeats local idempotency evidence. to_bundle does not establish profile validity, clinical correctness or permission to submit its export.

For explicitly composed FHIR execution, openmed.interop.fhir.FHIRWriteClient provides bounded conditional and transaction submission over injected custody, authority, approval, lineage, ledger and transport boundaries. Its FHIR transaction, conditional write and concurrency contracts do not convert legacy clients automatically or install a default transport.

Public namespaces and compatibility

Use documented explicit imports for the following contracts:

Namespace Role
openmed.agent.permissions Signed capability grants, purpose-bound access tickets and non-amplifying delegation
openmed.agent.tools Minimum-data schema lint, projections and argument classification
openmed.agent.approvals Signed, single-use human approval verification
openmed.agent Controlled outcomes, reviewer handoffs, policy matrices and run evidence
openmed.agent.workflows Bounded evidence workflows and recovery contracts
openmed.interop.fhir_capability_preflight Local content-free FHIR R4 capability checks
openmed.interop.omop Mutation batches, staging and database previews, vocabulary gates and the explicitly composed SQLite transactional adapter
openmed.interop.lineage Digest-bound FHIR-to-OMOP write lineage
openmed.interop.omop_rollback_manifest Structural rollback coverage without rollback execution

These are additive adoption paths. A legacy label is not a deprecation notice or permission to remove an import in a minor release. Follow the public API policy and release compatibility policy: announce deprecations with DeprecationWarning, a replacement and changelog entry; retain at least one full minor-version warning window, and defer breaking removal to a major release. Existing root imports remain supported according to that policy. Do not import private helpers as migration replacements.

FHIR and OpenMRS: separate intake from dispatch

The following fragment is not executed. Its clients and records are owned by the application; the variables represent synthetic fixtures. It shows the legacy live-write switches so they can be found in a code review.

# Fragment: application-owned clients and synthetic fixtures; not executed.
fhir_client.put_resource(synthetic_resource, write=True)
fhir_client.fetch_and_deidentify("Observation", synthetic_id, write=True)
openmrs_adapter.write_back(synthetic_records, dry_run=False)

Do not equate write=False or dry_run=True with a grant, verified consent or human approval. FHIR fetches still perform reads; OpenMRS planning results and FHIR return values may contain resource content, identifiers or destination metadata. Keep those values local rather than copying complete results or raw exceptions into traces.

Before obtaining write credentials or materializing a proposed resource, check the declared interaction against cached capability metadata. This runnable synthetic example contrasts a read-only declaration with update support:

# Runnable: synthetic cached capability metadata; no server contact.
from openmed.interop.fhir_capability_preflight import (
    FHIRWriteInteraction,
    FHIRWritePlan,
    preflight_write_plan,
)

statement = {
    "resourceType": "CapabilityStatement",
    "fhirVersion": "4.0.1",
    "rest": [
        {
            "mode": "server",
            "resource": [{"type": "Observation", "interaction": [{"code": "read"}]}],
        }
    ],
}
plan = FHIRWritePlan(
    interaction=FHIRWriteInteraction.UPDATE, resource_type="Observation"
)
refused = preflight_write_plan(statement, plan)
assert not refused.is_compatible
statement["rest"][0]["resource"][0]["interaction"] = [{"code": "update"}]
compatible = preflight_write_plan(statement, plan)
assert compatible.is_compatible
print({"refused": not refused.is_compatible, "compatible": compatible.is_compatible})

Compatibility means the cached declaration supports an interaction. It is not server-side resource validation or authorization. Review/incompatible outcomes must stop automatic write dispatch. See FHIR preflight and SMART scope auditing.

Minimum data precedes materialization

Verify the signed capability and the active ticket for the exact run, purpose, selectors and tool/action pair. Pass the verified ticket's data classes to the projection planner. This metadata-only fixture demonstrates the planner; its class tuple is not a verified ticket or a grant.

# Runnable: declared synthetic schema, including an insufficient-scope refusal.
from openmed.agent.tools import DataProjectionDeniedError, plan_data_projection

purpose = "purpose:org.example/synthetic-migration@1.0.0"
data_class = "data:org.example/synthetic-evidence@1.0.0"
schema = {
    "type": "object",
    "x-openmed-purpose": purpose,
    "properties": {
        "evidence": {
            "type": "string",
            "x-openmed-purpose": purpose,
            "x-openmed-minimum-data": "required",
            "x-openmed-data-class": data_class,
        }
    },
    "required": ["evidence"],
    "additionalProperties": False,
}
projection = plan_data_projection(
    schema,
    workflow_purpose=purpose,
    granted_data_classes=(data_class,),
)
assert projection.field_paths == ("/evidence",)
try:
    plan_data_projection(schema, workflow_purpose=purpose, granted_data_classes=())
except DataProjectionDeniedError:
    denied = True
else:
    denied = False
assert denied
print(
    {
        "projected_fields": len(projection.field_paths),
        "insufficient_scope_denied": denied,
    }
)

Only after these checks should trusted code read records, materialize the permitted fields and apply the declared argument policy. See capability grants, access tickets, minimum-data projections and argument classification.

OMOP: stage, review and commit

This runnable before example performs a legacy write only in an in-memory SQLite database. All source labels are synthetic, the note text is empty and only aggregate row counts are printed:

# Runnable: synthetic legacy loader/write boundary in memory only.
from openmed.interop.omop import load_grounded_notes, write_omop_sqlite

tables = load_grounded_notes(
    [
        {"note_id": "synthetic-note", "person_id": "synthetic-person", "text": ""},
    ]
)
connection = write_omop_sqlite(tables)
try:
    print(dict(tables.row_counts))
finally:
    connection.close()

The other persistence choices use the same in-memory tables. This fragment is not executed; connections and the destination are local application objects, not values supplied by an agent:

# Fragment: legacy persistence alternatives; not executed.
from openmed.interop.omop import write_omop_duckdb, write_omop_parquet

write_omop_duckdb(tables, local_duckdb_connection)
write_omop_parquet(tables, local_output_directory)

append and replace_by_note are persistence semantics, not governance modes. Do not translate replacement into unrestricted deletes or assume deterministic row IDs establish permission. DuckDB and Parquet require their documented optional dependencies; this guide installs neither.

The runnable after example stages one independent synthetic insert and checks a duplicate-insert refusal without dispatching a committer. It does not convert all loader tables automatically:

# Runnable: synthetic staged mutation previews; no commit.
from openmed.interop.omop import OmopMutation, OmopMutationBatch

mutation = OmopMutation.insert("person", {"person_id": 42})
batch = OmopMutationBatch((mutation,))
preview = batch.preview()
assert preview.is_valid
duplicate = OmopMutationBatch((mutation, mutation)).preview()
assert not duplicate.is_valid
print(
    {
        "operation_counts": dict(preview.operation_counts),
        "duplicate_denied": not duplicate.is_valid,
    }
)

A deployment must translate its intended rows into explicit operations, preserve source-span lineage, supply existing row identities for reference checks and use its locally governed vocabulary snapshot. Prepare and validate rollback coverage before review. The actual before-images or keys remain in protected local storage; a digest is a binding, not a backup or proof of semantic reversibility.

This fragment is not executed. The reviewed digest and receipt digest must come from an independently verified review boundary. Deriving them from an unreviewed preview does not create human approval.

# Fragment: independently verified review and application-owned commit; not executed.
from openmed.interop.omop import VocabularyWriteGate

rollback_manifest.validate(batch, current_local_vocabulary_snapshot)
approval = batch.bind_approval(
    preview,
    approved_preview_digest=reviewed_preview_digest,
    approval_receipt_digest=verified_receipt_digest,
)
result = VocabularyWriteGate(current_local_vocabulary_snapshot).commit(
    batch,
    trusted_atomic_committer,
    approval=approval,
    mappings=locally_selected_mapping_provenance,
)

The application must recheck authority, approval validity, current reference and vocabulary state at dispatch. The committer supplies atomicity and durable commit evidence; the staging and vocabulary helpers do not open a database or authenticate a reviewer. See staged mutations, vocabulary gates, write lineage, rollback coverage and approval tokens.

Transaction exports remain artifacts

This runnable fixture produces a transaction export locally. The Basic resource and source seed are synthetic, and only its type and entry count are printed. Profile validity is not asserted.

# Runnable: synthetic transaction artifact; no HTTP submission.
from openmed.clinical.exporters.fhir import to_bundle

bundle = to_bundle(
    (
        {
            "resourceType": "Basic",
            "id": "synthetic-resource",
            "code": {"text": "synthetic-export"},
        },
    ),
    doc_id="synthetic-bundle",
)
assert bundle["type"] == "transaction"
print({"bundle_type": bundle["type"], "entry_count": len(bundle["entry"])})

Protect the exported resources and per-entry request metadata. A transaction Bundle type, export callback or successful profile check does not supply a capability grant, purpose ticket, approval or bounded sender. Do not introduce a network call as an implicit export side effect.

Cutover and recovery

  1. Inventory every HTTP/database/file effect and retain each legacy boundary explicitly until the replacement is composed and validated.
  2. Use exact tools, actions, policies and purposes; refuse absent, expired or incompatible authority before materializing data or obtaining write credentials.
  3. Bind review to the exact preview and current context. A changed plan requires fresh review rather than an edited receipt.
  4. Preserve distinctions between denial, abstention, review, failure and unknown commit state. Do not turn uncertainty into success or automatic retry.
  5. Reconcile durable effects by the same idempotency key after interruption. Retry only proven-absent effects under still-valid approval; compensating clinical writes remain separately reviewed proposals.
  6. Validate synthetic refusals, replay and deployment evidence against every release gate before the deployment owner enables production effects.

See durable recovery, controlled outcomes, agent release gates and deployment assurance. Regression examples are technical evidence, not clinical validation, certification or authorization to deploy.