Skip to main content

Bulk Import

Wayd can load PPM data in bulk from CSV files through a set of import endpoints. These are useful for standing up an environment, migrating from another tool, or seeding realistic data. 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. Calls can also be made 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.

How imports work​

A few rules apply to every import:

  • References are by natural key where a record has one, otherwise by id. A project is referenced by its Key and a person by their employee number, because those are keys the record genuinely has. Portfolios, programs, themes, categories and lifecycles are referenced by id: their names are not uniquely indexed, so a name is a display value that may match more than one record. A run reports the id of the record each applied row created, so one import's results are what the next file references.
  • 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 its page under Settings → System → Imports shows what became of each row.
  • 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 a row kept out only because another row in its group was rejected is reported the same way the real run would. 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, so a set of files posted under one value (a whole portfolio's worth, say) shows as one batch in Settings → System → Imports, with summed counts and each run's page linking to the others. The group is a label, not a link: a program still names its portfolio by the id the portfolio run reported, so the files are posted in order and each run waited for exactly as before. Leave it out to submit a file on its own. An empty GUID is rejected rather than treated as a group.
  • 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, rows reference each other by it (a KPI names its initiative's ImportId, a task its parent's), and leaving it blank uses the row's position instead. It is never written onto the records the import creates.
  • Group by group. A rejected row — a reference that does not exist, a record that already exists, or a transition the domain refuses — keeps out its own group and nothing else, and the run names the row and the reason. What a group is differs by import; see Limits.
  • A malformed file is refused whole. The checks in the column tables below — a required cell left blank, a value too long, a status or type that is not one of those listed, a date a row's status does not allow — and the checks across the file — a name or key repeated within the file, a parent cycle among tasks, a KPI naming an initiative the file does not contain — are made before anything is queued. A file failing any of them returns 422 naming the problem, and no run is created; a cell that cannot be read at all (text in a date or id column) returns 400. Fix the file and post it again.
  • 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.
  • Multi-value columns are semicolon-separated. Columns that hold a list (role assignments, strategic themes, project keys) separate their values with semicolons — for example E-1001;E-1002. A comma would collide with the CSV field delimiter.
  • 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.
  • Transition dates say when a record moved, and are separate from its timeline. CreatedOn, ActivatedOn and ClosedOn record when the record reached each status; Start/End, where a record has them, are the timeline it plans to run over. A project that overran closed after the end it planned for, so the two are not interchangeable. CreatedOn is required on every portfolio, program and project row; which of the other two a row may carry depends on its status, and a date for a status the record never reached is rejected rather than ignored. They must also run forwards: CreatedOn ≤ ActivatedOn ≤ ClosedOn.
  • Status columns are case-insensitive and use the status names without spaces (for example Active, OnHold, NotStarted). Status, state, type, priority and direction cells are required: a blank one fails the file, so write the value out even where it is the usual one.
  • A missing file returns 422, not 400 — the file is a validation failure like any other.

Imports create records through the normal domain rules, exactly as if you had used the app — so the same constraints apply (a program can only be created inside an active or on-hold portfolio, a project needs a lifecycle before approval, and so on).

Import order​

Because records reference each other, and because the domain enforces an order (see PPM Lifecycle Rules), imports must be run in dependency order:

Reference data (expenditure categories, project lifecycles — created in Settings)
→ Strategic themes
→ Portfolios
→ Programs
→ Projects
→ Project tasks
→ Project stage statuses
→ Strategic initiatives (with KPIs)
→ Finalize (close historical programs and portfolios)

Two ordering points are worth calling out:

  • Reference data comes first. Expenditure categories and project lifecycles are managed in Settings and are not imported — create them there first, because projects reference them by id. A lifecycle must be active before a project can be assigned it.
  • Historical work is finished last. The domain only lets you add work to an active program or portfolio, but only lets you close one when everything inside it is already closed. So programs and portfolios that should end up Completed/Closed/Archived are imported active, their contents are loaded, and then the finalize import closes them.

Permissions​

Each import requires the Import permission for the relevant resource:

ImportPermission
Strategic themesImport Strategic Themes
Portfolios · FinalizeImport Portfolios
ProgramsImport Programs
Projects · Tasks · StagesImport Projects
Strategic initiatives (+ KPIs)Import Strategic Initiatives

Limits​

Every PPM import applies group by group: the rows for one record go in together or not at all, and a rejected row keeps out only its own group. The other groups are still imported, and the run finishes as Partially succeeded with the reason on each row it refused. A row kept out only because another row in its group was rejected says so and names that row, so the fix is always to one row.

ImportA group isOne rejected row keeps out
Portfoliosone portfoliothat portfolio
Programsone programthat program
Projectsone projectthat project
Tasksone projectthat project's whole breakdown
Stagesone projectthat project's other stage changes
Strategic initiativesone initiative, with its KPIsthat initiative and its KPIs
Finalizeone program or portfoliothat one record

Tasks and stages group by the project rather than by the row, because rows within a project are not independent of one another: a child task names its parent's ImportId in the same file and task numbers advance as rows apply, and a project's stages are read against one another.

Every PPM import accepts at most 10,000 rows per file, for the import itself and for a check (validateOnly=true) alike. A file over the cap is rejected before anything is queued, with a message naming the cap. Applying group by group saves in stages rather than in one transaction, so the cap is no longer what bounds a run, but it stays where it was until a larger file has been proven end to end.

Splitting a larger file follows the same grouping: split a task or stage file on project boundaries, never inside a project. Post the parts one at a time, waiting for each run; one submissionGroupId across them keeps the runs findable as one import.

The rules every file shares — request size, ImportId length, how long the post waits, retries, stalls and retention — are in Settings → System → Imports → Limits and rules.


Strategic themes import​

POST /api/strategic-management/strategic-themes/import

Loads strategic themes, which programs and projects reference by id. Only active themes can be attached to programs and projects.

ColumnRequiredNotes
NameYesUnique within the file and against existing themes. Max 64 chars.
DescriptionYesMax 1024 chars.
StateYesProposed, Active, or Archived. Set directly on the new theme.

Portfolios import​

POST /api/ppm/portfolios/import

Loads portfolios. A portfolio has no planned timeline of its own — its date range is set entirely by its lifecycle transitions — so the row carries those dates directly and they are applied as the portfolio is walked to its status. There is no closing date column, for the reason in the note below.

ColumnRequiredNotes
NameYesUnique within the file and against existing portfolios. Max 128 chars.
DescriptionYesMax 1024 chars.
StatusYesProposed, Active, OnHold, Closed, or Archived. A row naming Closed or Archived is imported active — see below.
CreatedOnYesThe date the portfolio was proposed.
ActivatedOnConditionalRequired unless Proposed; not allowed on a Proposed row. The date the portfolio was activated, which becomes the start of its date range.
SponsorsNoSemicolon-separated employee numbers.
OwnersNoSemicolon-separated employee numbers.
ManagersNoSemicolon-separated employee numbers.
Closed and archived portfolios

A portfolio destined to be Closed or Archived is imported active here (with its start date) and closed by the finalize import after its programs and projects have been loaded — because a portfolio cannot close until its contents are closed.

Programs import​

POST /api/ppm/programs/import

Loads programs into their portfolio. A program is created inside its portfolio, so the portfolio must exist (and be active) first. There is no closing date column, for the reason in the note below.

ColumnRequiredNotes
NameYesUnique within the file and against existing programs. Max 128 chars.
DescriptionYesMax 2048 chars.
PortfolioIdYesThe owning portfolio, by id.
StatusYesProposed, Active, Completed, or Canceled. A row naming Completed or Canceled is imported active — see below.
StartConditionalThe planned timeline. Start and End must both be empty or both set. Required for any program that was activated.
EndConditionalMust be on or after Start.
CreatedOnYesThe date the program was proposed.
ActivatedOnConditionalRequired for Active and Completed; optional for Canceled (a program can be canceled before or after it started); not allowed on a Proposed row.
StrategicThemesNoSemicolon-separated theme ids (must be active).
SponsorsNoSemicolon-separated employee numbers.
OwnersNoSemicolon-separated employee numbers.
ManagersNoSemicolon-separated employee numbers.
Completed and canceled programs

Like portfolios, a program that should end up Completed or Canceled is imported active and closed by the finalize import, since a program can only be closed once all its projects are closed.

Projects import​

POST /api/ppm/projects/import

Loads projects. Key is the project's own natural key (which tasks and initiatives reference), so it must be unique. The portfolio, program, expenditure category, lifecycle, and themes are all referenced by id.

ColumnRequiredNotes
NameYesMax 128 chars.
DescriptionYesMax 4096 chars.
KeyYes2–20 uppercase letters/numbers. Unique across all projects.
PortfolioIdYesThe owning portfolio, by id.
ExpenditureCategoryIdYesAn expenditure category, by id. Create in Settings first.
StatusYesProposed, Approved, Active, Completed, or Canceled.
ProgramIdNoA program in the same portfolio, by id. Empty means portfolio-direct.
ProjectLifecycleIdConditionalA lifecycle, by id. Required for Approved projects and for any project that will have tasks.
BusinessCaseNoMax 4096 chars.
ExpectedBenefitsNoMax 4096 chars.
StartConditionalThe planned timeline. Start and End must both be empty or both set. Required for active/completed projects.
EndConditionalMust be on or after Start.
CreatedOnYesThe date the project was proposed. An Approved project is dated by this too — the row carries no separate approval date.
ActivatedOnConditionalRequired for Active and Completed; optional for Canceled (a project can be canceled before or after it started); not allowed on Proposed or Approved.
ClosedOnConditionalRequired for Completed and Canceled — the status says which it records; not allowed on the rest.
StrategicThemesNoSemicolon-separated theme ids (must be active).
Sponsors · Owners · Managers · MembersNoSemicolon-separated employee numbers.

Unlike portfolios and programs, a project has nothing beneath it that must close first, so it reaches its real final status during the run. Its status history is dated from these columns rather than from when the file was uploaded, so an imported project reads the same as one driven through the app.

Project tasks import​

POST /api/ppm/projects/tasks/import

Loads the work breakdown for projects. One file can cover many projects — each row names its project by key. A child task references its parent row by that row's ImportId, or an existing task by id. Rows may be listed in any order; parents are always created before their children, and a parent cycle is rejected.

ColumnRequiredNotes
ProjectKeyYesThe project this task belongs to, by key.
NameYesUnique among the file's rows for the same project. Max 128 chars.
DescriptionNoMax 2048 chars.
StageNameYesA project stage, by name — created from a stage of the project's assigned lifecycle, so the name is unique within the project.
ParentImportIdNoThe ImportId of the row this task nests under. Empty makes this a root task of its stage.
ParentTaskIdNoAn existing task to nest under, by id. Cannot be combined with ParentImportId.
TypeYesTask or Milestone.
StatusYesNotStarted, InProgress, Completed, or Canceled. A milestone cannot be InProgress.
PriorityYesLow, Medium, High, or Critical.
ProgressConditionalPercent complete (0–100). Required for tasks; not allowed for milestones.
PlannedStart · PlannedEndConditionalFor tasks: both empty or both set; PlannedEnd on or after PlannedStart. Not allowed for milestones.
PlannedDateConditionalRequired for milestones: the milestone's date. Not allowed for tasks.
EstimatedEffortHoursNoGreater than 0 when present.
AssigneesNoSemicolon-separated employee numbers.

Project stage statuses import​

POST /api/ppm/projects/stages/import

Sets the status of project stages. Stages are created from the stages of the project's assigned lifecycle and start as Not Started; this import sets each one's status.

ColumnRequiredNotes
ProjectKeyYesThe project, by key.
StageNameYesA project stage, by name — created from a stage of the project's lifecycle.
StatusYesNotStarted, InProgress, Completed, or Canceled.
Applied exactly as given

This import sets stage status to exactly the value supplied — it does not derive a stage's status from its tasks. Whatever produces the file decides what each stage's status should be, so you keep full control. (The data-generation tool rolls each stage's status up from its tasks and emits these rows for seeding.)

Strategic initiatives import​

POST /api/ppm/strategic-initiatives/import

Loads strategic initiatives, optionally with their KPIs in the same call. The initiatives CSV is the file field; the KPIs CSV, if provided, is a second kpiFile field — sent together so the KPIs are attached before each initiative is driven to its final status (a closed initiative accepts neither new projects nor new KPIs).

Initiatives (file):

ColumnRequiredNotes
NameYesUnique within the file and against existing initiatives. Max 128 chars.
DescriptionYesMax 2048 chars.
PortfolioIdYesThe owning portfolio, by id.
StatusYesProposed, Approved, Active, Completed, or Canceled. (OnHold is not supported by import: a row naming it is rejected.)
StartYesInitiative start date.
EndYesOn or after Start.
ProjectKeysNoSemicolon-separated project keys the initiative delivers through.
Sponsors · OwnersNoSemicolon-separated employee numbers.

KPIs (kpiFile):

ColumnRequiredNotes
StrategicInitiativeImportIdYesThe ImportId of the initiative row this KPI belongs to (must be in the same import). Where that row left the column blank its position stands in, so 1 reaches the first initiative — but a file carrying KPIs should supply ImportId, since inserting a row silently re-parents every KPI below it.
NameYesUnique within its initiative. Max 64 chars.
DescriptionNoMax 512 chars.
TargetValueYesThe target value that defines success.
StartingValueNoThe baseline value. Below TargetValue for Increase, above it for Decrease.
PrefixNoSymbol shown before the value (e.g. $). Max 8 chars.
SuffixNoSymbol shown after the value (e.g. %). Max 8 chars.
TargetDirectionYesIncrease or Decrease.

The KPIs file has no ImportId column of its own.

Finalize import​

POST /api/ppm/portfolios/finalize/import

Closes out programs and portfolios after their contents have been imported — the last step of a PPM import. This exists because the domain only lets you close a program once all its projects are closed, and a portfolio once all its programs and projects are. So historical work is imported active, and this file closes it. Program rows are applied before portfolio rows regardless of order, since a portfolio cannot close while one of its programs is still open.

ColumnRequiredNotes
TypeYesProgram or Portfolio.
IdYesThe program or portfolio this row closes, by id, per Type.
StatusYesPrograms: Completed or Canceled. Portfolios: Closed or Archived.
EndDateConditionalRequired for portfolio rows (becomes the portfolio's end date); ignored for program rows.