Skip to main content

Bulk Import

Wayd can load planning data in bulk from CSV files through three import endpoints — one for planning intervals, one for the objectives on them, and one for risks. Each 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 or a script. 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.

These imports take IDs, not names

Unlike the PPM and Organization imports, which resolve everything by human-readable natural key, these reference records by GUID — teams, people and planning intervals — and use numeric ids for status, category, impact and likelihood.

A file therefore cannot be authored by hand without first looking those values up, typically from the API. Treat these as machine-generated formats rather than something to type out.

How imports work​

A few rules apply to all three:

  • A successful post returns the run, not the records it created. The 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 — follow it in Settings → System → Imports, or by its id against the import endpoints.
  • 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 endpoint takes an optional submissionGroupId query parameter — any GUID you choose — and record it on the run, so a set of files posted under one value shows as one batch in Settings → System → Imports, with summed counts and each run's page linking to the others. Leave it out to submit a file on its own. An empty GUID is rejected rather than treated as a group.
  • A malformed file is still rejected whole, before anything is queued. Every row is validated on the way in, so a file with a bad row never becomes a run. What happens once rows start being applied is covered under what a failed run leaves behind.
  • 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.
  • ImportId is your key for a row. 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. Leave the column blank and the row's position in the file is used instead, so a hand-authored file works without it.
  • ImportId is not written onto what the import creates. It identifies a row of your file, not a record in Wayd. The durable link runs the other way: each applied row reports the id of the record it created.
  • Instant columns are UTC and use ISO 8601 (2026-01-15T00:00:00Z). Date-only columns take yyyy-MM-dd — 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.

What a failed run leaves behind​

All three imports are all-or-nothing. The file is applied as one unit, so a rejected row means none of it is written. Correct the file and submit it again.

You do not need to work out what happened by re-posting. Open the run in Settings → System → Imports to see each row's outcome, then:

  • Retry rejected rows once the underlying problem is fixed. Rows that already succeeded are never reapplied, so this cannot duplicate them.
  • Resume a run that stopped partway — cancelled, or interrupted — to apply only the rows it never reached.
Row data is kept for 30 days

Retrying a row re-applies the data you submitted, which is held for 30 days after a run finishes and then discarded. After that a row can no longer be retried and the file must be submitted again.

Permissions​

ImportPermission
Planning intervalsImport Planning Intervals
Planning interval objectivesImport Planning Interval Objectives
RisksImport Risks

Limits​

ImportRows per fileApplied
Planning intervals10,000All-or-nothing
Planning interval objectives10,000All-or-nothing
Risks10,000All-or-nothing

A file over its cap is rejected before anything is queued, with a message naming the cap. Split a larger file and post the parts one at a time, waiting for each run; one submissionGroupId across them keeps the runs findable as one import. An all-or-nothing file is one database transaction, and 10,000 rows bounds it.

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.


Planning intervals import​

POST /api/planning/planning-intervals/import

Loads planning intervals together with the teams that ran them. Planning intervals belong to an agile release train rather than to the organization, so a customer arriving with history brings dozens of them — one row each, with no second file for the teams.

ColumnRequiredNotes
ImportIdNoYour key for the row; outcomes are reported against it. Unique within the file. Defaults to the row's position when blank.
NameYesMax 128 chars. Unique within the file, and rejected if an interval already has it.
DescriptionNoMax 2048 chars.
StartYesyyyy-MM-dd.
EndYesyyyy-MM-dd. On or after Start.
IterationWeeksYesLength of each generated iteration, in whole weeks. Greater than 0.
IterationPrefixNoPrefixes each generated iteration's name. Max 32 chars.
TeamIdsNoSemicolon-separated GUIDs of the teams that ran the interval. See below — blank means no teams.

This import only creates, so TeamIds never replaces a roster​

A row whose Name already exists is rejected, and since the file is all-or-nothing that rejects the whole file. Nothing is ever updated. That is what makes TeamIds safe to put on the same row: it can only be the roster a brand-new interval starts with.

So a blank TeamIds leaves the new interval with no teams. It does not clear anything, because there is nothing there to clear. Add the teams afterwards with Manage Teams on the interval, or fix the column and submit the file again.

A row naming a team Wayd has not finished replicating out of Organization is rejected rather than dropped, and names the ids it could not find. Import teams before intervals, and give a team created moments earlier a few seconds to land.

Iterations are generated from the cadence, not imported​

There is no column for iterations. Each interval's are generated from IterationWeeks and IterationPrefix the way a hand-created interval's are: iterations of that length running from Start, named {IterationPrefix}{number}, with the last one truncated to End and categorized Innovation and Planning.

An interval whose real iterations ran to irregular lengths, or were named something else entirely, will therefore not import faithfully. Correct it afterwards with Manage Dates on that interval, which is the only place iteration names, categories and date ranges are editable.

Planning interval objectives import​

POST /api/planning/planning-intervals/objectives/import

Loads objectives onto planning intervals that already exist — created by hand or by the planning intervals import above. Each row names its own interval in PlanningIntervalId, so one file can carry objectives for as many intervals as you have: an organization's whole planning history goes in a single file rather than one per interval.

Because the file is all-or-nothing, a row naming an interval or a team that does not exist, or an interval whose objectives are locked, rejects the whole file — including the rows for every other interval in it.

ColumnRequiredNotes
ImportIdNoYour key for the row; outcomes are reported against it. Unique within the file. Defaults to the row's position when blank.
PlanningIntervalIdYesGUID of the planning interval the objective belongs to. Rows may name different intervals.
TeamIdYesGUID of the team the objective belongs to.
NameYesMax 256 chars.
DescriptionNoMax 1024 chars.
StatusIdYes1 Not Started · 2 In Progress · 3 Completed · 4 Canceled · 5 Missed.
ProgressYesPercent complete, 0–100.
StartDateNoyyyy-MM-dd. Must be before TargetDate where both are given.
TargetDateNoyyyy-MM-dd.
IsStretchYestrue or false.
ClosedDateUtcConditionalUTC instant. Required when the status is Completed, Canceled or Missed, and must be empty otherwise.
OrderNoDisplay position within the team's objectives.

Risks import​

POST /api/planning/risks/import

Loads risks. A row naming a team, reporter or assignee that Wayd does not have is rejected, which, as the file is all-or-nothing, rejects the whole file.

ColumnRequiredNotes
ImportIdNoYour key for the row; outcomes are reported against it. Unique within the file. Defaults to the row's position when blank.
TeamIdYesGUID of the team the risk belongs to.
SummaryYesMax 256 chars.
DescriptionNoMax 1024 chars.
ReportedOnUtcYesUTC instant, and must be in the past.
ReportedByIdYesGUID of the employee who reported it. Not nullable — a blank cell fails the file.
StatusIdYes1 Open · 2 Closed.
CategoryIdYes1 Resolved · 2 Owned · 3 Accepted · 4 Mitigated.
ImpactIdYes1 Low · 2 Medium · 3 High.
LikelihoodIdYes1 Low · 2 Medium · 3 High.
AssigneeIdNoGUID of the employee it is assigned to.
FollowUpDateNoyyyy-MM-dd.
ResponseNoMax 1024 chars.
ClosedDateUtcConditionalRequired when StatusId is 2 (Closed), and must be empty otherwise. Must be after ReportedOnUtc and in the past.