Skip to content

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

GET https://training.gov.au/api/training/{code}/releases/{releaseNumber}/unitgrid

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 to false in unitgrid ingestion; if/when we need the fallback flag, the LLM parser extracts it.
  • rules[1].count — taken from qualifications.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, not pool.length.

Running the ingestion

Full corpus (one-time)

CF_API_TOKEN=... node tools/parse-packaging-rules.mjs --unitgrid
  • 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-fields enrich 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)

CF_API_TOKEN=... node tools/parse-packaging-rules.mjs --unitgrid --canary

Runs against BSB40920, CHC50121, SIT40521 only. Writes to D1 (the canary output IS ground truth — no dry-run needed in unitgrid mode).

Single qual

CF_API_TOKEN=... node tools/parse-packaging-rules.mjs --unitgrid --qual BSB40920

Force re-run

CF_API_TOKEN=... node tools/parse-packaging-rules.mjs --unitgrid --force

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:

  1. Deleted qualsqualifications.status = 'Deleted'. TGA keeps the metadata but the unit grid is gone.
  2. Superseded quals where TGA retired the release detail — rare but observed.
  3. Quals with no unit grid at all — some legacy skill sets historically stored as qual_tree kind.

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_number first. If the row has NULL, the script skips it with a no release_number in qualifications row log line. Re-run tools/tga-enrich-quals.mjs --version-fields to 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-fields rerun. The script handles these as failed and they can be recovered by a second run because the default filter picks up unprocessed rows.
  • isEssential: null rows 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.