Bulk Import
Wayd can load organization data in bulk from CSV files through a set of import endpoints. These are
useful for standing up an environment, onboarding a company's people and teams, or migrating from an HRIS or
another tool. Each import is a single POST of one CSV file (multipart/form-data, field name file), and
each is gated by a permission.
Every import described here can be run from Settings → System → Imports,
which lists each file's columns and hands over a template. Files can also be posted directly to the
endpoints below (for example with curl, a script, or the
wayd-data data-generation tool, which generates these
exact CSVs and posts them for you). Exact request/response shapes are also in the API reference the API
serves at /scalar.
However it was submitted, the run is visible in Settings → System → Imports, where you can see what became of each row and act on a run that did not finish. See Imports.
How imports work
A few rules apply to every import:
- References are by natural key, not ID. A CSV cannot carry Wayd's generated IDs, so rows reference each other by human-readable keys — an employee by their employee number, a team by its code, and a role by its name. A manager, a team parent, or a staffing assignment is expressed with those keys.
- A successful post returns the run, not the records it created. Every file is applied by a
background worker, and the post waits a few seconds for it: a small file comes back finished
(
200 OK) with its counts, and a large one comes back still queued or running (202 Accepted) with aLocationto follow. Either way the body is the same run, and it has its own page under Settings → System → Imports. - A file can be checked before it is imported. Add
validateOnly=trueto the post, or choose Check File in Settings → System → Imports, and every row goes through the import's own checks without anything being created: each comes back passed or rejected with its reason, and an all-or-nothing file reports every rejection rather than stopping at the first step that finds one. It is advice, not a promise — the data can change before you import — and a finished check can be imported for real from its page without posting the file again. - Files posted together can be grouped. Every import endpoint takes an optional
submissionGroupIdquery parameter — any GUID you choose — and records it on the run. Post a set of files under one value and Settings → System → Imports shows them as one batch with summed counts, and each run's page links to the others; thewayd-dataseed does this for each run. Leave it out to submit a file on its own. An empty GUID is rejected rather than treated as a group. - Teams, hierarchy and staffing are all-or-nothing; employees are applied row by row. For the three team imports, any row that is rejected — an unknown employee number, team code, or role name, or a team name or code that is already taken — fails the entire import, and nothing is saved; the run shows each rejected row and why. The employee import instead rejects the offending row (a duplicate employee number or email, say) and keeps the rest, so a run can finish partially succeeded; the run shows which rows were rejected and why, and they can be retried once fixed. See Limits.
ImportIdis yours, and its cells may be blank. Any value you like — a row number, a key from whatever system the file came out of — provided it is unique within the one file, compared case-insensitively. Row outcomes are reported against it, and leaving it blank uses the row's position instead. It is never written onto the records the import creates; the durable link runs the other way, as the id of the record each applied row reports creating.- Every column must be present, even the optional ones. Required in the tables below means the cell
needs a value; it does not mean the column can be left out. A missing header fails the file with
Header with name 'X' was not found. Include every column and leave unused cells blank. - Dates are ISO
yyyy-MM-dd. A date column takes a date, not a timestamp: a cell carrying a time is rejected rather than truncated. - A missing file returns 422, not 400 — the file is a validation failure like any other.
- Boolean and status columns are case-insensitive and, where a column is optional, have a sensible default (noted per column below).
- Multi-value columns are semicolon-separated. A cell holding more than one value separates them with
;— for exampleada@acme-legacy.example;a.lovelace@acme-legacy.example. A comma would collide with the CSV field delimiter. Values are trimmed, and blanks are ignored.
Imports create records through the normal domain rules, exactly as if you had used the app. A useful side effect: creating an organization record also replicates it into the PPM, Planning, and Work areas (for example, creating a team makes it available to those areas), because the import goes through the same domain path the app uses.
Import order
Because records reference each other, imports must be run in dependency order:
Employees
→ Teams (and Teams of Teams)
→ Team hierarchy (team memberships)
→ Team staffing (team members)
- Employees first — teams are staffed with them, and employees reference their manager by employee number (a manager may appear anywhere in the same file or already exist).
- Teams before hierarchy and staffing — the team hierarchy places teams under one another, and staffing puts employees on teams, so both need the teams to exist.
- Roles must already exist — the staffing import resolves each row's role by name and fails if it does not exist. Create the team member roles you intend to use before importing staffing. (The data-generation tool creates any missing roles for you as a separate step before staffing.)
Permissions
| Import | Permission |
|---|---|
| Employees | Import Employees |
| Teams | Import Teams and Teams of Teams |
| Team staffing · Team hierarchy | Manage Team Memberships |
Limits
| Import | Rows per file | Applied |
|---|---|---|
| Employees | 50,000 | Row by row |
| Teams | 10,000 | All-or-nothing |
| Team hierarchy | 10,000 | All-or-nothing |
| Team staffing | 10,000 | All-or-nothing |
A file over its cap is rejected before anything is queued, with a message naming the cap. Split a larger
export into several files and post them one at a time, waiting for each run; one submissionGroupId
across the parts keeps them findable as one import. The caps differ because an all-or-nothing file is one
database transaction, and 10,000 rows bounds it. A check of an employees file is one transaction too, so a
preflight takes at most 10,000 employees at a time.
The rules every file shares — request size, ImportId length, how rows are chunked, how long the post
waits, retries, stalls and retention — are in Settings → System → Imports → Limits and rules.
Employees import
POST /api/organization/employees/import
Loads employees and their management chain. A manager is referenced by employee number and may appear anywhere in the same file or already exist — the import resolves managers after creating everyone, so order within the file does not matter. An employee whose manager cannot be resolved is still created (without a manager) rather than failing the batch, and the row carries a warning saying so.
A row is rejected, and the rest of the file still imported, when its employee number already belongs to an
employee or to an earlier row in the file, or when any of its addresses (Email or AdditionalEmails) is
already an employee's primary or additional address or is claimed by an earlier row. Employee numbers and
addresses are compared case-insensitively. A row that fails the column rules in the table below — a missing
name, a malformed address, a value too long — fails the whole file when it is posted, before anything is
queued.
A file holds at most 50,000 employees, so a larger export goes in several files, posted one at a time
and each waited for. A manager resolves only if they are in the same file or already exist: a report
whose manager arrives in a later file is created without one, and the link is not made afterwards.
Order the files so managers precede their reports where you can, and post them under one
submissionGroupId so the runs read as one import.
| Column | Required | Notes |
|---|---|---|
EmployeeNumber | Yes | The employee's natural key, unique across the company. Max 256 chars. |
FirstName | Yes | Max 100 chars. |
MiddleName | No | Max 100 chars. |
LastName | Yes | Max 100 chars. |
Email | Yes | A valid email address. Max 256 chars. |
AdditionalEmails | No | Other work addresses this person is known by, semicolon-separated. Each must be a valid address, max 256 chars, and must not repeat the row's own Email. |
HireDate | No | The date the employee was hired. Unlike the other date columns, this one also accepts a timestamp, read as UTC. |
JobTitle | No | Max 256 chars. |
Department | No | Max 256 chars. |
OfficeLocation | No | Max 256 chars. |
ManagerNumber | No | The manager's employee number. May reference someone elsewhere in the file. |
IsActive | No | Whether the employee is current. Defaults to true. |
EmployeeType | No | Free-form worker type (e.g. Employee, Contractor, Intern), mirroring an HRIS descriptor. Max 256 chars. |
Set IsActive to false to record someone who has left. Inactive employees are created active and then
deactivated through the domain, so the record is consistent — but note that an inactive employee cannot
hold a current team membership, so leave them out of the staffing import.
AdditionalEmails records other addresses the same person is known by — typically an address they used
before a domain or tenant move. Wayd matches people against connected systems on email, so an Azure DevOps
work item still stamped with someone's previous address resolves to them once that address is listed here.
Record work addresses only. Home and personal addresses serve no purpose for matching and should never be loaded into Wayd.
If a people-sync connection (Workday or Microsoft Entra ID) is active, it owns employee email addresses: each sync replaces them with exactly what the source reports, so addresses added by import are removed unless the source reports them too. Use this column for tenants without a people connector, or as a one-off backfill.
Teams import
POST /api/organization/teams/import
Loads teams and teams of teams in one file. The Type column discriminates between the
two; both share the same shape. Teams are flat here — the parent/child structure is set separately by the
team hierarchy import.
| Column | Required | Notes |
|---|---|---|
Type | Yes | Team or TeamOfTeams (case-insensitive). |
Name | Yes | Max 128 chars. Unique across teams and teams of teams, compared case-insensitively. |
Code | Yes | The team's natural key: 2–10 uppercase letters/numbers. Unique across teams and teams of teams. |
Description | No | Max 1024 chars. |
ActiveDate | Yes | The date the team became active. |
IsActive | No | Whether the team is current. Defaults to true. |
InactiveDate | Conditional | Required when IsActive is false; must be after ActiveDate. Must be empty when active. |
A file that repeats a name or a code fails when it is posted. A row whose name or code already belongs to a
team is rejected, which fails the whole import. Each team is created with the same default operating model
as one created in the app — Kanban and Count, with the time zone, grace period and working days from the
scheduling settings — and each team of teams with an operating model in
the scheduling settings' time zone. A row with IsActive set to false is created active and then
deactivated on its InactiveDate.
Team hierarchy import
POST /api/organization/teams/team-memberships/import
Places teams under one another to build the org hierarchy — a value-stream / ART / team shape, for example. Each row puts a child (a team or a team of teams) under a parent (which must be a team of teams) for a date range.
| Column | Required | Notes |
|---|---|---|
ChildCode | Yes | The child team's code. May be a Team or a Team of Teams. |
ParentCode | Yes | The parent's code. Must be a Team of Teams. Cannot equal ChildCode. |
Start | Yes | When the membership begins. |
End | No | When it ends. Must be on or after Start. Blank for a membership still in place. |
Because a child may itself be a team of teams, chaining rows builds multiple tiers (for example a team under an ART, and that ART under a value stream). The batch is all-or-nothing and rejects a code no team has, a parent that is not a team of teams, a child or parent that is inactive, a membership that overlaps another of the child's parent memberships (already in Wayd or earlier in the file), and a parent that is already beneath the child, which would make a cycle.
Team staffing import
POST /api/organization/teams/members/import
Staffs teams — places employees on teams in named roles. Each row is one employee on one team in one role; to give someone multiple roles on the same team, add multiple rows.
| Column | Required | Notes |
|---|---|---|
TeamCode | Yes | The team or team of teams, by code. |
EmployeeNumber | Yes | The employee, by number. Must be an active employee. |
RoleName | Yes | A team member role, by name (case-insensitive). The role must already exist. Max 128 chars. |
Rows add to whoever is already on the team: a person already a member gains the roles the file gives them. A row giving someone a role they already hold on that team is rejected, which fails the whole import.
Two things fail the whole batch if not satisfied: the role must already exist (staffing does not create roles — resolve them by name), and the employee and team must both be active (the domain does not allow staffing an inactive person or team). Create any needed roles first, and omit former employees.
Related
- Data generation tool — generates all of these CSVs and seeds them for you, in the correct order (and creates any missing roles).
- PPM Bulk Import — the equivalent imports for portfolios, programs, projects, and more.
- Planning Bulk Import — planning interval objectives and risks.
- Product Management Bulk Import — the product catalog.
- Organizations — the teams, memberships, and staffing these imports populate.