Bulk Import via CSV
Bulk Import via CSV
If you already have your school's data in a spreadsheet, the Bulk Import page lets you paste or upload a CSV instead of entering everything one record at a time on each individual page.
Import order matters
There's one tab per entity: Teachers, Buildings, Rooms, Subjects, Classes, Lessons — in that
order for a reason. Lessons reference Teachers, Rooms, Subjects, and Classes by name, so those
have to exist first (either entered manually or imported) before a Lessons import can resolve
them. Buildings comes before Rooms because a Room's building column references one by name.
Each tab has a download template option showing the expected columns and a couple of example rows — the fastest way to get the exact header names right.
Insert-only, not update
Importing a row that collides with an existing unique name (Rooms, Subjects, Classes, and Departments all require unique names within your school) skips that row and reports why, rather than updating the existing record. Re-running an import after fixing a few bad rows is safe — anything that already succeeded just gets skipped again as a duplicate on the second pass. But if you intentionally change a value in your spreadsheet and re-import, it will not update the existing record to match; you'd need to edit that record by hand or delete-and-reimport it.
Teachers are the one exception — their names have no uniqueness check (see
Adding Teachers), so importing the same teacher name twice creates two
separate teacher records rather than being skipped as a duplicate. The only thing that stops a
teacher row is its optional employeeId: if that ID is already used by another teacher (ignoring
upper/lower case), the row is skipped and reported. So giving your teachers Employee IDs makes a
repeated Teachers import safe too.
Cross-references are matched by name, case-insensitively
A Lesson row's subject, teachers, and groups columns are looked up by name against what's
already in your school's data (not by any internal ID, since a spreadsheet has no way to know
those). Matching is case-insensitive. If a name in your CSV doesn't match anything, that row is
skipped and reported (e.g. "Unknown teacher(s): ...") rather than creating a new record on the
fly.
A Class row's parent column works the same way — the parent has to already exist, either from
an earlier row in the same file or from a previous import/manual entry.
A Room row's building column is the one exception to "matched by name, skipped if no
match" — if the name doesn't match an existing Building, one is
created on the fly instead of the row being rejected. This is deliberate: unlike a Lesson's
teacher/subject/group references (which must already exist as real records elsewhere), a
Building is little more than a label, so there's no useful "unknown building" error to report.
Import the Buildings tab first only if you want to set up cross-building travel penalties
(on the Buildings page) before importing Rooms — otherwise it's optional, since Rooms import
creates any building it references automatically.
Multiple values in one cell: use ;, not a comma
Where a column can hold more than one value — a Lesson's teachers or groups (for co-teaching
or a combined session), a Room's tags — separate them with a semicolon, not a comma. This is
deliberate: a comma is CSV's own field separator, so using it inside a cell would need quoting;
semicolons avoid that entirely and match how these same multi-value fields are stored internally.
What each tab imports
| Tab | Required columns | Notes |
|---|---|---|
| Teachers | name | email is optional and does not send an invite email — bulk-importing shouldn't fire a wave of invite emails at once. Use Resend Invite on the Teachers page per person afterward if you want to invite them. employeeId is optional; when given it must be unique within your school (not case-sensitive), including against earlier rows in the same file — a row whose ID is taken is skipped. Unavailable periods can't be set via CSV; add those manually after import. |
| Buildings | name | Nothing else to import — cross-building travel penalties are set on the Buildings page after import. Optional tab: importing Rooms with a building value creates any building it references automatically. |
| Rooms | name, capacity | tags optional, ;-separated. building optional — matched by name, created if it doesn't already exist (see note above). |
| Subjects | name | Nothing else to import. |
| Classes | name, size | parent optional. Even a parent/Year-level row needs a positive size value in the file, even though that number is never actually used once the row has children — see Class Groups, Years & Subgroups. List a parent row before its children in the file. |
| Lessons | subject, teachers, groups | duration/count default to 1 if omitted. bunching defaults to SPREAD (valid values: SPREAD, CONSECUTIVE, NONE). requiredRoomTags/activityTags optional, ;-separated. Locked and Preferred time slots can't be imported — a Period is identified by day + time, not a stable name you can type into a spreadsheet cell, so those are set on the Lessons page after import. |
After importing
Each row's outcome (created / skipped / error, with a reason) is reported per row, not just as a pass/fail for the whole file — check that list before assuming everything went in as expected, especially on a large file where a handful of rows commonly fail on a name typo or a missing prerequisite record.