Upload a CSV
Columns it understands
Matched by header name, never by position, so column order does not matter.
Common headers work too: "course" and "competency" land in training, "certificate number" in cert_ref. People and competencies have to exist first; the dry run names every row it would skip and why.
Why there is no expiry column
An expiry is computed from the competency's own validity. Accepting one from a spreadsheet would mean either ignoring it silently or letting the file overrule the rule the rest of the product enforces.
Each row is recorded the way the grid records one: a date in the future, training that does not apply to the person, or an archived person is refused. The same person, training and date twice is skipped, so running the same file again adds nothing.