flow

Revit Projection Archive and Reuse

Retiring a ParameterKey archives its Revit projection instead of deleting it, so the shared-parameter GUID survives and re-adding the parameter reuses the same identity. One path breaks the rule, and it is documented here.

FlowStatus: Implemented in api-v1

Overview

RevitParameterProjection retirement archives. It does not delete, and the reason is a single immutable value: the shared-parameter GUID.

A shared parameter’s GUID is its identity inside every Revit model that has ever received it. A .txt shared parameter file that ships a new GUID for the same concept produces a second, unrelated parameter in the model — the old one keeps its values and the new one starts empty. So BuildPlan treats the GUID as permanent: updateAuthoredFacts refuses to remove or replace it (REVIT_GUID_IMMUTABLE, BO-RP-INV-01), and retirement keeps the row that holds it.

That immutability is what makes archive strictly better than delete. Delete would strand every model that already carries the parameter. Archive plus search-then-reuse means a steward can retire a parameter, change their mind a year later, and the reinstated projection hands Revit back the identity it already knows.

OpenAPI

Retirement and reuse are event-driven; there is no steward “archive” or “reuse” endpoint in the normal workflow. The projection archive endpoint exists only as maintenance.

OperationRouteRole
archiveParameterKeyPOST /parameter-keys/{id}/actions/archiveRaises ParameterKeyArchived, which always archives the projection
updateParameterKeyPOST /parameter-keys/{id}/actions/updateDropping usage:bim_parameter retires the projection through the same handler
approveParameterKeyPOST /parameter-keys/{id}/actions/approveThe reinstatement trigger — ensure finds the archived row and reuses it
deleteParameterKeyDELETE /parameter-keys/{id}The one exception. Hard-deletes the key and cascade-deletes the projection
archiveRevitParameterProjectionPOST /revit/parameter-projections/{id}/archiveMaintenance only — x-internal, tagged system-maintenance
createRevitParameterProjectionPOST /revit/parameter-projectionsMaintenance only. Also reuses an archived row when one is linked — see the caveat below

Event Path

ArchiveRevitProjectionOnParameterKeyRetirementHandler is registered on both ParameterKeyUpdated and ParameterKeyArchived in bim-ontology.composition.ts. Same handler instance, two registrations.

Its shouldArchive is:

EventDecision
ParameterKeyArchivedtrue, always — no key reload
ParameterKeyUpdatedReload the key. false if the key is not found; otherwise true when lifecycleStatus !== 'approved' or usage:bim_parameter is absent

Two properties follow. First, because the key lifecycle is linear (approved → archived only), a real-world ParameterKeyUpdated that trips the status half of the guard is a key still in draft or active — the guard is mostly there for the usage drop. Second, the guard is the exact complement of ParameterKeyUpdatedProjectionEnsureHandler’s, so a single update either ensures or archives, never both. See Parameter Key to Revit Projection.

The handler then loads the linked projection and stops early when there is none or it is already archived. Unlike the ensure handlers, it has no failure recorder: on a failed archive it logs Archive Revit projection after ParameterKey retirement failed with the projection id and error code, then rethrows. Nothing is stamped onto the parameter_keys row.

The seven projection events have no in-module subscriber

RevitParameterProjectionCreated, Updated, Approved, Demoted, Reused, Archived, and Resynced are all dispatched, and no behavioural handler is registered for any of them anywhere in the module. The only thing that receives them is the wildcard DomainEventAuditLogger, registered on '*' when composition builds its own dispatcher — it logs and does nothing else.

Every consequence in this flow is driven by a ParameterKey* event flowing into the projection side, never by a projection event flowing out. Treat the seven as emitted-but-unsubscribed until a consumer appears.

Search-then-reuse

EnsureRevitProjectionForParameterKeyUseCase never blindly creates. It searches first, and the search includes archived rows: findByParameterKeyId resolves the revit_parameters link on the parameter_keys record and applies no status filter at all.

When the found row is archived and the key still carries bim_parameter:

  1. requireApprovedParameterKey runs before any write. An unapproved key gets PARAMETER_KEY_NOT_APPROVED and the archived row stays archived — an unapproved key cannot resurrect a projection.
  2. reuseArchivedProjection calls reactivateFromArchive(now), which returns a new aggregate with status: 'draft' and records RevitParameterProjectionReused. Everything else — id, parameterKeyId, definition and GUID, spec type, storage type, visibility flags, discipline, applications — is carried over verbatim.
  3. The row is saved, derived category sets are re-persisted, and the event is dispatched.

reactivateFromArchive is a no-op returning the same instance when the projection is not archived, so the path is safe to re-enter.

When the found row is not archived, ensure returns it untouched. When nothing is linked, ensure creates — the create path is the only one that mints a GUID.

archived has no forward transition. updateAuthoredFacts, approve, and demoteToDraft all refuse an archived projection with REVIT_PROJECTION_ARCHIVED (BO-RP-INV-06, HTTP 409). Reuse into draft is the only way out, and the reused projection must pass RevitProjectionApprovePolicy again before it re-enters any Revit output.

One caveat on the maintenance create path

CreateRevitParameterProjectionUseCase also reuses an archived row when the key already links to one — but on that path the reuse happens after an identity comparison and without a key-approval check. Ensure applies requireApprovedParameterKey before reuse; the raw POST /revit/parameter-projections maintenance endpoint does not. Reaching it requires calling the internal create endpoint directly with a matching definition form.

The one place GUID preservation does not hold

DeleteParameterKeyUseCase hard-deletes the linked projection:

const projection = await this.deps.projections.findByParameterKeyId(
command.id as string,
);
const cascadedRevitProjectionId = projection?.id ?? null;
if (projection) {
await this.deps.projections.deleteById(projection.id);
}

apps/api-v1/src/modules/bim-ontology/core/application/use-cases/ParameterKeyAuthoringUseCases.ts lines 324–330. The row is removed, the GUID goes with it, and it is never reissued. The response reports the removed id as cascadedRevitProjectionId. The port documents the intent explicitly: deleteById says callers “must not filter by projection status — delete is allowed in any status (D10: only when the ParameterKey itself is deleted).”

This is the single place in the module where the GUID-preservation rule does not apply. Say so plainly rather than implying archive is universal.

Two things limit the damage:

  • Assignments block it. listByParameterKeyId runs first; any surviving ClassParameter assignment produces ParameterKeyHasClassParameterAssignmentsError — an HTTP 409 whose details list the blocking assignment domain_ids so a steward unassigns first. Assignments are deliberately not cascade-deleted, because that would destroy steward work silently.
  • Reachability. In practice delete is therefore reachable mainly for unassigned keys — a mistyped or abandoned key, which is exactly the case where discarding a GUID costs nothing. A key that ever reached real classes has assignments to clear first, and clearing them is a visible act.

An already-deleted key returns { alreadyDeleted: true, cascadedRevitProjectionId: null } and a 204, so the operation is idempotent.

The Airtable side matches: parameter_key_actions.js documents Delete as “cascade-deletes the linked Revit projection on the API. This script never reads or writes a Revit row.”

Why not soft-delete the ParameterKey too?

Archive already exists on the key (approved → archived) and is the ordinary retirement. DELETE is the escape hatch for a key that should never have existed, and the design accepts that using the escape hatch forfeits the GUID. If a GUID needs to survive, archive the key; do not delete it.

Code References

  • apps/api-v1/src/modules/bim-ontology/adapters/inbound/events/ArchiveRevitProjectionOnParameterKeyRetirementHandler.ts
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/RevitProjectionAuthoringUseCases.tsArchiveRevitParameterProjectionUseCase, EnsureRevitProjectionForParameterKeyUseCase, reuseArchivedProjection, requireApprovedParameterKey
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/ParameterKeyAuthoringUseCases.tsDeleteParameterKeyUseCase, lines 295–344
  • apps/api-v1/src/modules/bim-ontology/core/application/ports/outbound/ForStoringRevitParameterProjections.ts — the deleteById contract
  • apps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitParameterProjection.tsarchive, reactivateFromArchive, REVIT_GUID_IMMUTABLE
  • apps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitParameterProjectionStatus.ts
  • apps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitSharedParameterGuid.ts, SharedParameterDefinition.ts — the value that must never be recycled
  • apps/api-v1/src/modules/bim-ontology/adapters/outbound/persistence/airtable/AirtableRevitParameterQueryAdapter.tsfindByParameterKeyId, which does not filter on status
  • apps/api-v1/src/modules/bim-ontology/adapters/outbound/persistence/airtable/AirtableRevitParameterRepository.tscountPhysicalLinksForParameterKey, deleteById
  • apps/api-v1/src/modules/bim-ontology/composition/bim-ontology.composition.ts — the two archiveOnRetirement registrations
  • apps/airtable-front-end/current/bim-ontology/parameter_key_actions.js

ADRs

ADRBearing on this flow
Platform ADR-0047Airtable scripts call the API; retirement and reuse are handler work, not script work
Platform ADR-0048domain_id at the public boundary; the cascade result reports a domain_id
Platform ADR-0052The projection’s active was renamed approved; legacy active rows rehydrate as approved, so archived-then-reused rows normalize cleanly