Skip to main content

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.

Screen or API

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 a Location to 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=true to 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 submissionGroupId query 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; the wayd-data seed 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.
  • ImportId is 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 example ada@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​

ImportPermission
EmployeesImport Employees
TeamsImport Teams and Teams of Teams
Team staffing · Team hierarchyManage Team Memberships

Limits​

ImportRows per fileApplied
Employees50,000Row by row
Teams10,000All-or-nothing
Team hierarchy10,000All-or-nothing
Team staffing10,000All-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.

Splitting a large export

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.

ColumnRequiredNotes
EmployeeNumberYesThe employee's natural key, unique across the company. Max 256 chars.
FirstNameYesMax 100 chars.
MiddleNameNoMax 100 chars.
LastNameYesMax 100 chars.
EmailYesA valid email address. Max 256 chars.
AdditionalEmailsNoOther 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.
HireDateNoThe date the employee was hired. Unlike the other date columns, this one also accepts a timestamp, read as UTC.
JobTitleNoMax 256 chars.
DepartmentNoMax 256 chars.
OfficeLocationNoMax 256 chars.
ManagerNumberNoThe manager's employee number. May reference someone elsewhere in the file.
IsActiveNoWhether the employee is current. Defaults to true.
EmployeeTypeNoFree-form worker type (e.g. Employee, Contractor, Intern), mirroring an HRIS descriptor. Max 256 chars.
Former employees

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.

Additional emails

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.

ColumnRequiredNotes
TypeYesTeam or TeamOfTeams (case-insensitive).
NameYesMax 128 chars. Unique across teams and teams of teams, compared case-insensitively.
CodeYesThe team's natural key: 2–10 uppercase letters/numbers. Unique across teams and teams of teams.
DescriptionNoMax 1024 chars.
ActiveDateYesThe date the team became active.
IsActiveNoWhether the team is current. Defaults to true.
InactiveDateConditionalRequired 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.

ColumnRequiredNotes
ChildCodeYesThe child team's code. May be a Team or a Team of Teams.
ParentCodeYesThe parent's code. Must be a Team of Teams. Cannot equal ChildCode.
StartYesWhen the membership begins.
EndNoWhen it ends. Must be on or after Start. Blank for a membership still in place.
Multi-tier hierarchies

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.

ColumnRequiredNotes
TeamCodeYesThe team or team of teams, by code.
EmployeeNumberYesThe employee, by number. Must be an active employee.
RoleNameYesA 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.

Roles and active employees

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.