---
name: halo-asset-import-spreadsheet
description: Spreadsheet-driven bulk asset writes — a user-reviewable .xlsx/.csv (one row per asset, AF/CF-named columns) is the plan artifact, and the MCP write pipeline is the engine that applies it. Covers the spreadsheet column contract, ingest→compose→apply→verify flow, and the empirically proven limits of Halo's NATIVE spreadsheet importer (no status control, matching-field prerequisites, and the destructive framework-CF boundary) for the narrow cases where the native importer is still worth using. Load whenever a user supplies a spreadsheet of assets to create or update, asks for an "import spreadsheet", or a bulk asset operation needs a reviewable file-based plan.
---

# halo-asset-import-spreadsheet — Spreadsheet plans, MCP writes

## References

| Reference | Type | Location | When to Use |
|---|---|---|---|
| `halo-bulk-update` | Skill | `plugins/grcitsm-analyst/skills/halo-bulk-update/` | Six-phase methodology (plan file, approval gate, pilot, audit) — this skill supplies the plan artifact and payload source |
| `halo-action-pipeline` | Skill | `plugins/grcitsm-analyst/skills/halo-action-pipeline/` | The plan→apply engine that executes what the spreadsheet describes |
| `halo-ssp-asset-write-conventions` | Skill | `plugins/grcitsm-analyst/skills/halo-ssp-asset-write-conventions/` | Framework-CF semantics, CFBaseline two-transaction rule, KSI family-ID rule |
| `make_import_xlsx.py` | Script | `plugins/grcitsm-analyst/skills/halo-asset-import-spreadsheet/scripts/` | Deterministic CSV/JSONL → .xlsx generator (for producing review copies) |

A spreadsheet is the best interface for bulk asset data: one row per asset, one column per field, and a human can review 500 rows in Excel in a way they never can with 500 JSON payloads. This skill standardizes that interface — **the spreadsheet is the plan; the MCP catalog is the writer.** Halo's native spreadsheet importer is deliberately NOT the default write path: live trials (2026-07-15, UAT tenant) proved it cannot carry the framework CFs and silently destroys existing values when asked to (see the contract section) — while the MCP path writes everything, verifiably.

## The spreadsheet contract (what the user supplies or the agent generates)

One sheet, header row, one row per asset. Column names bind exactly to tenant fields:

- **Identity columns**: `AssetTag` (the durable identity / AFUUID, e.g. `IS-<CFISAcronym>`), `CustomerName`, `SiteName`, `DeviceType` (asset-type display name). Identity decides create-vs-update: rows whose tag resolves to an existing asset are updates; the rest are creates.
- **Asset Field columns**: `AF` + field label (`AFTitle`, `AFImplementation Statement`, `AFControl Origination`, …). Exact per-type labels come from the `asset_type_fields` tool (`[CF Name]` column).
- **Custom Field columns**: the CF name (`CFBaseline`, `CFAFedRAMPControls`, `CFAFedRAMPKSIs`, `CFACMMCControls`, `CFACMMCRequirements`, …). Multi-select values are comma-joined IDs (`AC-02,AC-03`); KSIs are family IDs (strip `.NN`).
- Include only columns being written; omitted fields are preserved by the MCP writer (scalar merge-on-omit).

## Flow: ingest → compose → apply → verify

1. **Ingest.** Read the user's .xlsx/.csv. Validate structurally before any write: identity columns present, `DeviceType` exists (`asset_types`), AF/CF headers resolve against `asset_type_fields`, lookup values against their allowed lists, framework-CF values against `lookup_controls` / `lookup_requirements`, tags unique. Report every rejected row — never silently skip.
2. **Resolve create-vs-update.** Bulk-resolve `AssetTag` → asset id via `asset_summary`. Existing → `update_asset`; missing → `create_asset` (+ status per type rules) then update.
3. **Compose, don't improvise.** For >~20 rows, hand the validated rows to the `halo-action-pipeline`: the planner composes `actions-<step>.jsonl` (one MCP call per line) honoring write-ordering rules — `CFBaseline` lands before framework CFs (separate transactions), Controls before Requirements (two-pass), read-merge-write on multi-select edits. ≤~20 rows may go direct per-row.
4. **Approval gate.** The spreadsheet itself (plus the composed action file) is what the user approves, per `halo-bulk-update` Phase 3.
5. **Apply** via the pipeline (`/grcitsm-analyst:halo-apply`) — resumable, audited, hang-proof.
6. **Verify.** Read back via `get_asset` spot checks + a full re-export diff against the source spreadsheet. Relationships are wired in the WIRE phase (`set_asset_relationships` / reconcile tools) — they never travel in the spreadsheet.

## Halo's NATIVE importer — measured contract (2026-07-15 live trials)

Halo ships a native asset importer (**Configuration > Asset Management > Import**). It can be a legitimate fast path for very large, **fields-only** payloads (it wrote 9-column narrative rows flawlessly once configured), but its contract is narrow and two of its failure modes are silent. Use it only with the user driving the UI and ALL of the following understood:

**Tenant prerequisites (both bit us in trials):**
1. **"Status of new assets"** (Asset Management settings) must be a status valid for the target type. There is NO status column in the import contract — a `Status` column is ignored whether name or code — and an invalid default fails every row with `Failed - Invalid Status`.
2. **Imports > "Unique identifier for Assets"** must be set (Asset Tag) and that column present, or every row creates a duplicate (`asset tag must be unique` on collision). Matching may normalize away spaces/hyphens — verify tag uniqueness under that normalization.

**What it carries reliably:** native columns, AF fields (lookups resolve from display text), long text verbatim, static-value-list CFs like `CFBaseline`, and match-updates against existing assets.

**What it must NEVER carry — framework CFs.** The SQL-backed, per-device lookup CFs (`CFAFedRAMP*`, `CFACMMC*`) cannot be written by the importer: correct display names AND correct lookup ids both produce an empty selection (the importer never executes the per-device value-list SQL). Worse, the write is full-replace: **a framework-CF column in an import file wipes existing values on every matched asset.** Verified three ways in live trials, including erasing values that had just been written via MCP. If a payload includes framework CFs, they go through `update_asset` after the field import — no exceptions.

## Worked example — Implementation Statement rebuild from the library

Rebuild all IS assets (dtype 191) from the `CTImplementationStatements` custom-table library:

1. Generate the per-site spreadsheet from the library export: one row per library row in the site's baseline scope, `AssetTag = IS-<CFISAcronym>` (+ site envelope suffix where used), narrative fields verbatim, `CFBaseline` per site. No framework-CF columns.
2. User reviews/approves the spreadsheet; fields land via pipeline `create_asset`/`update_asset` (or the native importer as the fields-only fast path).
3. Framework CFs from the library's per-framework columns via `update_asset` `[{name}]` arrays — baseline-scoped per site, Controls before Requirements.
4. Wire Component↔IS (rel 8) from the tag stem; IR→IS (rel 7) via `reconcile_ssp_relationships_ir_is` after the CFs land (tenant automations may pre-wire some); sweep IR rollups.
5. Re-export and diff against the library to zero.
