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.
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.
| Operation | Route | Role |
|---|---|---|
| archiveParameterKey | POST /parameter-keys/{id}/actions/archive | Raises ParameterKeyArchived, which always archives the projection |
| updateParameterKey | POST /parameter-keys/{id}/actions/update | Dropping usage:bim_parameter retires the projection through the same handler |
| approveParameterKey | POST /parameter-keys/{id}/actions/approve | The reinstatement trigger — ensure finds the archived row and reuses it |
| deleteParameterKey | DELETE /parameter-keys/{id} | The one exception. Hard-deletes the key and cascade-deletes the projection |
| archiveRevitParameterProjection | POST /revit/parameter-projections/{id}/archive | Maintenance only — x-internal, tagged system-maintenance |
| createRevitParameterProjection | POST /revit/parameter-projections | Maintenance 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:
| Event | Decision |
|---|---|
ParameterKeyArchived | true, always — no key reload |
ParameterKeyUpdated | Reload 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:
requireApprovedParameterKeyruns before any write. An unapproved key getsPARAMETER_KEY_NOT_APPROVEDand the archived row stays archived — an unapproved key cannot resurrect a projection.reuseArchivedProjectioncallsreactivateFromArchive(now), which returns a new aggregate withstatus: 'draft'and recordsRevitParameterProjectionReused. Everything else — id,parameterKeyId, definition and GUID, spec type, storage type, visibility flags, discipline, applications — is carried over verbatim.- 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.
listByParameterKeyIdruns first; any surviving ClassParameter assignment producesParameterKeyHasClassParameterAssignmentsError— an HTTP 409 whose details list the blocking assignmentdomain_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.tsapps/api-v1/src/modules/bim-ontology/core/application/use-cases/RevitProjectionAuthoringUseCases.ts—ArchiveRevitParameterProjectionUseCase,EnsureRevitProjectionForParameterKeyUseCase,reuseArchivedProjection,requireApprovedParameterKeyapps/api-v1/src/modules/bim-ontology/core/application/use-cases/ParameterKeyAuthoringUseCases.ts—DeleteParameterKeyUseCase, lines 295–344apps/api-v1/src/modules/bim-ontology/core/application/ports/outbound/ForStoringRevitParameterProjections.ts— thedeleteByIdcontractapps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitParameterProjection.ts—archive,reactivateFromArchive,REVIT_GUID_IMMUTABLEapps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitParameterProjectionStatus.tsapps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitSharedParameterGuid.ts,SharedParameterDefinition.ts— the value that must never be recycledapps/api-v1/src/modules/bim-ontology/adapters/outbound/persistence/airtable/AirtableRevitParameterQueryAdapter.ts—findByParameterKeyId, which does not filter on statusapps/api-v1/src/modules/bim-ontology/adapters/outbound/persistence/airtable/AirtableRevitParameterRepository.ts—countPhysicalLinksForParameterKey,deleteByIdapps/api-v1/src/modules/bim-ontology/composition/bim-ontology.composition.ts— the twoarchiveOnRetirementregistrationsapps/airtable-front-end/current/bim-ontology/parameter_key_actions.js
ADRs
| ADR | Bearing on this flow |
|---|---|
| Platform ADR-0047 | Airtable scripts call the API; retirement and reuse are handler work, not script work |
| Platform ADR-0048 | domain_id at the public boundary; the cascade result reports a domain_id |
| Platform ADR-0052 | The projection’s active was renamed approved; legacy active rows rehydrate as approved, so archived-then-reused rows normalize cleanly |