TGA Unit Grid Endpoint¶
First discovered: 2026-04-16 (STUDIO-PACKAGING-PARSE-01 reframe).
Canonical consumer: tools/parse-packaging-rules.mjs --unitgrid.
Storage target: qualification_packaging_rules in rtopacks-db, parser_version = 'unitgrid-v1'.
Related docs: source-of-truth-connectivity.md, tga-api-field-inventory.md.
Why this exists¶
TGA publishes the authoritative core/elective split for every qualification release via a dedicated endpoint. We didn't know about it until the STUDIO-PACKAGING-PARSE-01 swagger audit. This page is the canonical reference for the endpoint, its contract, and the ingestion pattern — so no future Claude burns hours rediscovering it or writes an LLM parser when the data is already structured.
The endpoint¶
Path params:
| Param | Type | Example | Notes |
|---|---|---|---|
code |
string | BSB40920 |
Qualification code. Case-sensitive in the URL but TGA accepts either. |
releaseNumber |
string | 1 |
Release number for that qual. Read from qualifications.release_number in rtopacks-db (populated by tools/tga-enrich-quals.mjs --version-fields). |
Auth: None. Public endpoint, no headers required.
Query params: None currently used. The swagger spec lists api-version with default 1.0, unused in practice.
Response: JSON array of unit objects, one per unit in the qualification's unit grid (both core and elective). No wrapper — the array itself is the body.
Response shape¶
Per swagger: schema UnitGridUsage, array of objects.
[
{
"code": "BSBPMG420",
"title": "Apply project scope management techniques",
"isEssential": true,
"isEssentialLabel": "Core",
"usageRecommendation": "current",
"usageRecommendationLabel": "Current",
"hasPreRequisites": false,
"links": [
{ "rel": "training-component", "href": "https://training.gov.au/api/training/bsbpmg420" }
]
},
{
"code": "BSBCRT411",
"title": "Apply critical thinking to work practices",
"isEssential": false,
"isEssentialLabel": "Elective",
"usageRecommendation": "current",
"usageRecommendationLabel": "Current",
"hasPreRequisites": false,
"links": [ ... ]
}
]
Key field: isEssential.
true→ unit is core (required for the qual).false→ unit is elective (part of the pool the RTO picks from).null→ skill set (not a qualification); filter these out upstream.
isEssentialLabel is the same information as a display string.
Field mapping to qualification_packaging_rules¶
The ingestion script transforms the flat array into the table's schema:
TGA unitgrid response → qualification_packaging_rules row
──────────────────────────────────────────────────────────────────────
units where isEssential=true → groups[0] (id="core", label="Core")
units where isEssential=false → groups[1] (id="elective_pool", label="Electives")
COUNT(isEssential=true) → rules[0] (type="fixed", group_id="core", count=N)
qualifications.elective_units_ → rules[1] (type="from_group", group_id="elective_pool",
count count=M, open_corpus_fallback=false)
qualifications.total_units_ → total_required
required
raw response JSON → raw_input
"high" → confidence
What we lose (deliberately): Group A / Group B / Group C / ... sub-structure. TGA does NOT publish this in any structured endpoint — it only exists in the prose HTML of content bundle item 0116 (packaging_rules). The script deliberately collapses all electives into a single elective_pool. For the canvas's first ship, a flat pool of real units per qual is the right foundation. When per-group picker filtering becomes a product requirement, turn on the LLM parser path (tools/parse-packaging-rules.mjs without --unitgrid) — canary already passed three quals at high confidence.
What we set conservatively:
open_corpus_fallback: false— the unitgrid endpoint doesn't tell us whether a qual allows "any current endorsed Training Package" substitution. It's only in the prose. Set tofalsein unitgrid ingestion; if/when we need the fallback flag, the LLM parser extracts it.rules[1].count— taken fromqualifications.elective_units_count(populated by the enrich pass). The unit grid lists more electives than the user actually has to pick — that's the whole point of a pool — so we use the declared elective count, notpool.length.
Running the ingestion¶
Full corpus (one-time)¶
- Default filter:
WHERE release_number IS NOT NULL AND qual_code NOT IN (SELECT qual_code FROM qualification_packaging_rules WHERE parser_version = 'unitgrid-v1'). - 5 concurrent TGA fetches with 100ms stagger — same pattern as the
--version-fieldsenrich pass. Effective ~50 req/sec against TGA, tolerated cleanly in production runs. - ~30 minute wall clock for ~8007 quals.
- Resumable: a crashed run picks up on the next invocation; only unprocessed rows are targeted.
Canary (three hand-verified quals)¶
Runs against BSB40920, CHC50121, SIT40521 only. Writes to D1 (the canary output IS ground truth — no dry-run needed in unitgrid mode).
Single qual¶
Force re-run¶
Reprocesses every qual regardless of whether it already has a unitgrid-v1 row. Use this when TGA has updated a qual's unit grid and we want a fresh pass, or when the transform logic has changed in a compatible way.
404 handling¶
Not every qual_code in qualifications has a live unitgrid endpoint. Three classes of 404:
- Deleted quals —
qualifications.status = 'Deleted'. TGA keeps the metadata but the unit grid is gone. - Superseded quals where TGA retired the release detail — rare but observed.
- Quals with no unit grid at all — some legacy skill sets historically stored as
qual_treekind.
Script behaviour: log the qual code, increment the 404 counter, skip the row, continue. Do not retry — the script finishes with a summary listing up to 20 codes and a pointer to this document.
What to do with 404s: generally nothing. They're real data gaps on TGA's side and will never come back. The canvas handles missing qualification_packaging_rules rows gracefully by falling through to the flat "Open electives — choose N" stub. If a specific deleted qual matters for teach-out, manual data entry or the LLM parser against historical packaging_rules HTML is the escape hatch.
Refresh cadence¶
Initial ingestion: one-time, full corpus (including superseded) — done once in session 62, 2026-04-16.
Ongoing: refresh only current quals. Run whenever tga-sync detects a qual has a new release. The script is idempotent per-qual (ON CONFLICT DO UPDATE on qual_code PK) and the default filter keeps reruns cheap.
Recommended cadence:
| Trigger | Command | Frequency |
|---|---|---|
Post-tga-sync new releases |
--unitgrid --force --qual <code> per changed code |
Per-release basis, integrated into the sync chain |
| Monthly audit of current quals | --unitgrid --force filtered by status='Current' |
Monthly, run manually |
| Incident re-ingestion (bad row found) | --unitgrid --force --qual <code> |
As needed |
The --force combined with a single --qual targets one row. Combined with no filter, it rewrites the whole corpus — only do this if the transform logic has changed.
Known gotchas¶
- Release number must be in
qualifications.release_numberfirst. If the row hasNULL, the script skips it with ano release_number in qualifications rowlog line. Re-runtools/tga-enrich-quals.mjs --version-fieldsto populate release numbers if needed. - Cold-start bursts on large runs. The first batch of a full run against TGA sometimes sees HTTP failures from rate limiting — same burst we saw in the
--version-fieldsrerun. The script handles these asfailedand they can be recovered by a second run because the default filter picks up unprocessed rows. isEssential: nullrows come from skill sets. The transform ignores them so they don't pollute either group.
What the endpoint does NOT give you¶
See ops/tga-api-field-inventory.md for the full picture, but at a glance:
- ❌ Group A / Group B / Group C sub-structure within electives.
- ❌ Selection rules (
choose 3 from Group A, 3 from A+B union, with open corpus fallback). - ❌ Open corpus fallback flag or constraint.
- ❌ Pre-requisite chains between units (use
/api/training/{unitCode}for that).
All of the above live in the packaging rules prose HTML (content bundle item 0116). The LLM parser in tools/parse-packaging-rules.mjs (no --unitgrid flag) is built and canary-verified for exactly this — turn it on when per-group picker filtering or open-corpus substitution becomes a product requirement.