---
name: halo-bulk-update
description: Six-phase methodology for any Halo bulk operation touching >20 assets/tickets — plan-to-file, master plan + decision items, user approval gate, single-row pilot, parallel writers with bounded scope + append-only audit log, end-of-run batch relationship wiring. Load whenever an agent is about to plan or execute a Halo bulk update/create at scale, or when running /ssp-ingest, /ssp-interview, or any operation that mutates a multi-row set. Phase mechanics detail lives in reference-docs/halo-bulk-update-operations.md.
---

# halo-bulk-update — Six-phase pipeline for >20-row Halo operations

## References

| Reference | Type | Location | When to Use |
|---|---|---|---|
| `ssp` | Skill | `plugins/grcitsm-analyst/skills/ssp/` | SSP asset spine workflow primer (per-row mechanics) |
| `grcitsm` | Skill | `plugins/grcitsm-analyst/skills/grcitsm/` | General Halo MCP catalog routing |
| `halo-ssp-asset-write-conventions` | Skill | `plugins/grcitsm-analyst/skills/halo-ssp-asset-write-conventions/` | Rule 1 read-merge-write, Rule 4 framework-agnostic Title |
| `halo-action-pipeline` | Skill | `plugins/grcitsm-analyst/skills/halo-action-pipeline/` | Plan→apply pipeline used when this six-phase methodology dispatches against tens of rows |
| `halo-asset-import-spreadsheet` | Skill | `plugins/grcitsm-analyst/skills/halo-asset-import-spreadsheet/` | Spreadsheet-as-plan intake for bulk ASSET writes (MCP-applied; native-importer limits documented) |
| Bulk operations detail | Reference | `reference-docs/halo-bulk-update-operations.md` | Phase mechanics: Array-Iteration runbook shapes, approval-gate surface paths, pilot procedures + catalog discovery, checkpoint protocol, staged-read discipline |
| Write semantics | Reference | `reference-docs/halo-platform/write-semantics.md` | Null-handling (§2), lookup rules (§3), framework-CF two-pass (§5), payload cliff (§8), structural writes (§12), bulk shape rules (§14) |
| API/MCP semantics | Reference | `reference-docs/halo-platform/api-mcp-semantics.md` | Relay semantics (§5), read-tool caps + stage-to-disk (§6) |

Operational primer for bulk Halo asset/ticket operations. Every phase exists because skipping it has caused data loss, silent drops, or stalled runs at scale; runs that follow all six phases complete with zero data loss.

## When to load this skill

- Composing or executing any Halo write operation that will touch >20 rows
- Running `/ssp-ingest` (which is implicitly bulk — Capabilities + Components + IRs across a Site)
- Designing a multi-asset reconciliation, rename, framework-CF rewire, or relationship rebind
- Resuming a partial-failure bulk run from `applied.jsonl`

For single-row or <20-row operations, defer to `[[ssp]]` (SSP-side) or `[[grcitsm]]` (general Halo MCP) — those skills cover the per-row mechanics. This skill is the orchestration layer on top.

## Phase 0 — Route the operation to the right channel

Before drafting anything, classify the operation. Three channels:

1. **Array-Iteration runbook (server-side)** — for a **uniform mechanical write**: the same operation across every row, no per-row reasoning or payload composition. One runbook (`auto_action:12/13` looping a native-SQL row set with Edit-Asset API-action bodies) applies the write to every row server-side — no agent fleet, no context bloat, no hang. Native SQL (`auto_action:18`) is read-only: it is the loop's QUERY, never the write. One real incident wrapped 2,068 identical column writes in 69-98 executors and froze silently. Runbook shapes + event-firing behavior: `reference-docs/halo-bulk-update-operations.md`.
2. **Spreadsheet-as-plan intake** — for asset create/update sets sourced from or reviewable as a spreadsheet: the .xlsx/.csv is the approval artifact and payload source (`[[halo-asset-import-spreadsheet]]`), and this pipeline applies it via MCP. Halo's NATIVE importer is only a narrow fields-only fast path — it cannot write framework CFs and wipes them if the columns are present (measured 2026-07-15); see that skill's contract section before ever using it. Relationships still go through the WIRE phase.
3. **Action-file + executor fleet (Phases 1-6 below)** — ONLY for writes that genuinely need agent-side judgment: a distinct, non-templatable payload per row, or a multi-tool sequence that can't be expressed as one iteration body.

## The six phases

```
1. DRAFT       per-partition plan files (one .md + one .jsonl per partition)
2. ASSEMBLE    master plan.md with metrics, samples, decision items
3. APPROVE     user gate — NO writes until explicit approval
4. PILOT       main agent does ONE row, verifies via get_asset
5. WRITE       N parallel writers with bounded scope, append-only audit log
6. WIRE        ONE batch set_asset_relationships at the end
```

### Phase 1 — Draft per-partition plans

Partition along a clean dimension that becomes parallel subagent scope:

| Operation | Natural partition |
|---|---|
| Implementation-Statement-spanning changes | per Capability |
| Implemented-Requirement-spanning changes | per Catalog (FedRAMP Rev5 / FedRAMP 20x KSI / CMMC) |
| Steampipe-mod-driven | per provider (AWS / Azure / EntraID) |
| Cross-site change | per Site |
| Asset-type-specific | per `assettype_id` |

One markdown file per partition: `outputs/<task>-YYYY-MM-DD/plan-<partition>.md`. Block per row with the FULL payload (Title, AFUUID, every field value, every customfield value).

**Always pre-parse to JSONL companion:** `plan-<partition>.jsonl`, one row per line with markdown stripped. Writers consume via `Read offset=N limit=1` — drastically cheaper than re-parsing markdown per row.

### Phase 2 — Assemble master plan

`plan.md` at the working-folder root:
- Top-level metrics (rows per partition, distinct scopes touched)
- Sample 2-3 rows per partition for visual sanity
- **Decision items aggregated** from drafter Notes sections, numbered (D1, D2, …) with options + recommended defaults
- Phase-2-deferred items (e.g. framework-CF wiring left for follow-up)
- Approval-gate wording

`all-rows-compact.md` — one-line scan view of every row.

### Phase 3 — User approval gate (NO writes until this passes)

Surface `plan.md` + `all-rows-compact.md` to the user and wait for explicit approval (`approve` / `approve with [decision overrides]` / `pause` / `cancel`). Overrides bake into writer prompts. Surface paths for main-loop vs subagent contexts: `reference-docs/halo-bulk-update-operations.md`.

### Phase 4 — Pilot one row

Main agent does ONE row manually to validate the full payload shape, then verifies via `get_asset` immediately. Mandatory for new asset types, new field combinations, lookup-value uncertainty, new relationship_id pairs, and **any write to a lookup-backed framework CF** (silent drops read back as `display:""` with `id:"0"` unchanged). Ticket-side framework-CF pilots additionally need a **delayed re-read (≥90s)** — async event runbooks can silently revert the write. Run the catalog-discovery procedure (`baseline_catalog` → `control_baselines_summary` → `lookup_controls` / `lookup_requirements`) BEFORE any lookup-backed-CF pilot. Full procedures: `reference-docs/halo-bulk-update-operations.md`. Value-format traps (zero-padding, KSI families, site-scope): `reference-docs/halo-platform/write-semantics.md` §3, §5.2.

Log the pilot to `applied.jsonl` so resume logic skips it.

### Phase 5 — Parallel writers with bounded scope

**Per-row context cost is ~14 KB tokens** (create + update full responses). A single writer caps at **10-15 rows** before hitting context limits.

Spawn N parallel writer subagents, each scoped to a partition or a 10-15-row slice. Strict prompt discipline:

- **NO verifications** (or sparse — every 10th, only if pilot didn't prove the pattern)
- **NO setup file reads** beyond the assigned JSONL line range
- **NO response echoing** — confirm minimally, move on
- **NO out-of-scope writes** — scope bounded positively by manifest/JSONL file, NEVER by id-blacklist
- **Halt on first failure** → write `blockers.md` → return cleanly

Every run maintains the mandatory `state/` checkpoint protocol: immutable `rows-<step>.jsonl` work list + append-only `tasks.jsonl` event log (start/done/fail per row × stage), plus the shared `applied.jsonl` outcome ledger. Small slices (≤5 rows full-stack, or one stage across ≤15 rows); orchestrator heartbeat on ledger growth (~10 min stall → kill + reassign); resume from last event per (row, stage) — creates are the only stage never retried blindly. Full event schema, standard stages, and heartbeat/resume rules: `reference-docs/halo-bulk-update-operations.md`.

### Phase 6 — End-of-run batch relationship wiring

Writers do NOT call `set_asset_relationships`. Concurrent per-row writes would race against replace-semantics on the parent (`reference-docs/halo-platform/write-semantics.md` §12, §14). Instead:

1. Accumulate new asset_ids in `applied.jsonl` (Phase 5 already does this).
2. Main agent reads applied.jsonl at end-of-run, builds union of new IDs.
3. ONE batch `set_asset_relationships` call on the parent asset — the subject (`asset_id` param) is the PARENT; entries in the relationships array are the children. Even ~190-row batches land in one call (sizing note in `reference-docs/halo-bulk-update-operations.md`).

## Sizing heuristics

| Scale | Approach |
|---|---|
| 1-5 rows | Inline in main agent. Pilot IS the run. |
| 6-20 rows | One subagent, sequential. |
| 21-50 rows | 1-2 partitions × 1 writer each. Pilot first. |
| 51-200 rows | 3-5 partitions × 1 writer each. Resume writers as needed. |
| 200+ rows | 3-5 partitions × N narrow-range writers (10-15 rows each). |

## Working folder layout

```
outputs/<task-slug>-YYYY-MM-DD/
  data/                  ← staged reads (pulled to disk before analysis/write) + manifest.json
  state/                 ← run state: rows-<step>.jsonl + tasks.jsonl checkpoint ledger
  README.md              ← scope, partitions, file map
  plan-<partition>.md    ← drafter output
  plan-<partition>.jsonl ← pre-parsed structured feed for writers
  plan.md                ← master plan + decision items
  all-rows-compact.md    ← one-line scan view
  applied.jsonl          ← append-only audit
  blockers.md            ← only if a writer halted; resume instructions
  relationships.json     ← Phase 6 batch payload
  parse_plan.py          ← markdown→jsonl parser, kept for audit
```

## Bulk reads stage to disk too (not just writes)

The stage-to-disk rule is corpus-owned: `reference-docs/halo-platform/api-mcp-semantics.md` §6 — land bulk pulls in `outputs/<run>/data/` with a `manifest.json` checkpoint, query the file with `jq`/`python`, never accumulate raw payloads in context, never re-pull per row inside parallel workers. Merge-on-omit means pre-reads are only needed for multi-select merges (`write-semantics.md` §2.1) — and at scale those pre-reads are staged once, not called inline per row. Bulk-run file layout, summary→point-read tool routing, and resume-from-manifest rules: `reference-docs/halo-bulk-update-operations.md` § Staged-read discipline.

## Single-call vs two-pass (framework CFs)

Corpus-owned: `reference-docs/halo-platform/write-semantics.md` §5.1 (Controls before Requirements, two-pass per row) and §14 (the ordering survives bulk scale; when framework CFs are preserved verbatim, a single `update_asset` per row suffices — Title / AFUUID / inventory_number changes don't retrigger the lookup filter).

## Anti-patterns

1. **Skip the user approval gate.** Always pilot, surface plan, wait.
2. **Writers calling `set_asset_relationships` per row.** Races against parallel writers on the same parent.
3. **Pulling tool responses into reasoning — reads AND writes.** 10 KB get_asset × 50 IRs = 500 KB context bloat; stage the pull to `outputs/<run>/data/` and query the file (see § Bulk reads stage to disk too). **Full-object write responses bloat identically:** a write tool that echoes the whole written entity returns ~6 KB INLINE on every call. That is UNDER the ~262 KB offload cap, so it is NOT auto-persisted — it lands in the executor's context and accumulates across the lease, slowing each write from ~2-3 s to ~30 s and degrading into an effective hang. The "write responses auto-persist, ignore them" assumption is wrong: auto-persist only holds ABOVE the offload cap. Measured at scale (2026-08-06, pre-slim `update_asset`): ~8k tokens per write response — roughly ~430k tokens had a 54-action run been held on one thread. For these, confirm minimally and discard the body; never read it back into reasoning. **Slim-projection write tools are the exception** (`update_asset` v1.0.0+ and the two `*_from_library` tools return `{id, inventory_number, client_id/name, site_id/name, status_id, inactive, written[]}`): the receipt is small, reading it whole is correct and cheap, and `written[]` makes a confirming read-back redundant. Discriminate by the response the DEPLOYED tool returns, not by tool name in a spec — a tenant still on the pre-slim version gets the fat object and the discard rule.
4. **Naming irrelevant IDs in subagent prompts.** Use manifest as positive scope; never id-blacklist.
5. **Re-deriving plans in writer prompts.** Drafter produced JSONL; writers read lines.
6. **Single monolithic writer for 100+ rows.** Decompose to 10-15 row batches.
7. **Mid-run convention changes.** If the shape changes, halt and update plan; don't have writers improvise.

## Cross-references

- `[[halo-ssp-asset-write-conventions]]` — Rule 1 read-merge-write, Rule 4 framework-agnostic Title
- `[[ssp]]` — SSP asset spine workflow primer (per-row mechanics)
- `[[grcitsm]]` — general Halo MCP catalog routing
- `update_asset` payload shape, multi-select CF array forms, validate-W Asset Field shapes, site-scope silent drops: `reference-docs/halo-platform/write-semantics.md` §2-§5.
- MCP relay cap + read-tool staging: `reference-docs/halo-platform/api-mcp-semantics.md` §6.
- Plan-to-file discipline: for any >5-row bulk create with complex content, write the full plan to `outputs/<task>/plan.md` and get user approval before any create call.
- Positive-scope rule: never name irrelevant ids in subagent prompts — bound each writer's scope by its manifest/JSONL slice only.
