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.
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 aLocationto 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=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 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
submissionGroupIdquery 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. 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, rows reference each other by it (a KPI names its initiative'sImportId, 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
422naming the problem, and no run is created; a cell that cannot be read at all (text in a date or id column) returns400. 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,ActivatedOnandClosedOnrecord 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.CreatedOnis 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:
| Import | Permission |
|---|---|
| Strategic themes | Import Strategic Themes |
| Portfolios · Finalize | Import Portfolios |
| Programs | Import Programs |
| Projects · Tasks · Stages | Import 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.
| Import | A group is | One rejected row keeps out |
|---|---|---|
| Portfolios | one portfolio | that portfolio |
| Programs | one program | that program |
| Projects | one project | that project |
| Tasks | one project | that project's whole breakdown |
| Stages | one project | that project's other stage changes |
| Strategic initiatives | one initiative, with its KPIs | that initiative and its KPIs |
| Finalize | one program or portfolio | that 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.
| Column | Required | Notes |
|---|---|---|
Name | Yes | Unique within the file and against existing themes. Max 64 chars. |
Description | Yes | Max 1024 chars. |
State | Yes | Proposed, 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.
| Column | Required | Notes |
|---|---|---|
Name | Yes | Unique within the file and against existing portfolios. Max 128 chars. |
Description | Yes | Max 1024 chars. |
Status | Yes | Proposed, Active, OnHold, Closed, or Archived. A row naming Closed or Archived is imported active — see below. |
CreatedOn | Yes | The date the portfolio was proposed. |
ActivatedOn | Conditional | Required unless Proposed; not allowed on a Proposed row. The date the portfolio was activated, which becomes the start of its date range. |
Sponsors | No | Semicolon-separated employee numbers. |
Owners | No | Semicolon-separated employee numbers. |
Managers | No | Semicolon-separated employee numbers. |
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.
| Column | Required | Notes |
|---|---|---|
Name | Yes | Unique within the file and against existing programs. Max 128 chars. |
Description | Yes | Max 2048 chars. |
PortfolioId | Yes | The owning portfolio, by id. |
Status | Yes | Proposed, Active, Completed, or Canceled. A row naming Completed or Canceled is imported active — see below. |
Start | Conditional | The planned timeline. Start and End must both be empty or both set. Required for any program that was activated. |
End | Conditional | Must be on or after Start. |
CreatedOn | Yes | The date the program was proposed. |
ActivatedOn | Conditional | Required for Active and Completed; optional for Canceled (a program can be canceled before or after it started); not allowed on a Proposed row. |
StrategicThemes | No | Semicolon-separated theme ids (must be active). |
Sponsors | No | Semicolon-separated employee numbers. |
Owners | No | Semicolon-separated employee numbers. |
Managers | No | Semicolon-separated employee numbers. |
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.
| Column | Required | Notes |
|---|---|---|
Name | Yes | Max 128 chars. |
Description | Yes | Max 4096 chars. |
Key | Yes | 2–20 uppercase letters/numbers. Unique across all projects. |
PortfolioId | Yes | The owning portfolio, by id. |
ExpenditureCategoryId | Yes | An expenditure category, by id. Create in Settings first. |
Status | Yes | Proposed, Approved, Active, Completed, or Canceled. |
ProgramId | No | A program in the same portfolio, by id. Empty means portfolio-direct. |
ProjectLifecycleId | Conditional | A lifecycle, by id. Required for Approved projects and for any project that will have tasks. |
BusinessCase | No | Max 4096 chars. |
ExpectedBenefits | No | Max 4096 chars. |
Start | Conditional | The planned timeline. Start and End must both be empty or both set. Required for active/completed projects. |
End | Conditional | Must be on or after Start. |
CreatedOn | Yes | The date the project was proposed. An Approved project is dated by this too — the row carries no separate approval date. |
ActivatedOn | Conditional | Required for Active and Completed; optional for Canceled (a project can be canceled before or after it started); not allowed on Proposed or Approved. |
ClosedOn | Conditional | Required for Completed and Canceled — the status says which it records; not allowed on the rest. |
StrategicThemes | No | Semicolon-separated theme ids (must be active). |
Sponsors · Owners · Managers · Members | No | Semicolon-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.
| Column | Required | Notes |
|---|---|---|
ProjectKey | Yes | The project this task belongs to, by key. |
Name | Yes | Unique among the file's rows for the same project. Max 128 chars. |
Description | No | Max 2048 chars. |
StageName | Yes | A project stage, by name — created from a stage of the project's assigned lifecycle, so the name is unique within the project. |
ParentImportId | No | The ImportId of the row this task nests under. Empty makes this a root task of its stage. |
ParentTaskId | No | An existing task to nest under, by id. Cannot be combined with ParentImportId. |
Type | Yes | Task or Milestone. |
Status | Yes | NotStarted, InProgress, Completed, or Canceled. A milestone cannot be InProgress. |
Priority | Yes | Low, Medium, High, or Critical. |
Progress | Conditional | Percent complete (0–100). Required for tasks; not allowed for milestones. |
PlannedStart · PlannedEnd | Conditional | For tasks: both empty or both set; PlannedEnd on or after PlannedStart. Not allowed for milestones. |
PlannedDate | Conditional | Required for milestones: the milestone's date. Not allowed for tasks. |
EstimatedEffortHours | No | Greater than 0 when present. |
Assignees | No | Semicolon-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.
| Column | Required | Notes |
|---|---|---|
ProjectKey | Yes | The project, by key. |
StageName | Yes | A project stage, by name — created from a stage of the project's lifecycle. |
Status | Yes | NotStarted, InProgress, Completed, or Canceled. |
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):
| Column | Required | Notes |
|---|---|---|
Name | Yes | Unique within the file and against existing initiatives. Max 128 chars. |
Description | Yes | Max 2048 chars. |
PortfolioId | Yes | The owning portfolio, by id. |
Status | Yes | Proposed, Approved, Active, Completed, or Canceled. (OnHold is not supported by import: a row naming it is rejected.) |
Start | Yes | Initiative start date. |
End | Yes | On or after Start. |
ProjectKeys | No | Semicolon-separated project keys the initiative delivers through. |
Sponsors · Owners | No | Semicolon-separated employee numbers. |
KPIs (kpiFile):
| Column | Required | Notes |
|---|---|---|
StrategicInitiativeImportId | Yes | The 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. |
Name | Yes | Unique within its initiative. Max 64 chars. |
Description | No | Max 512 chars. |
TargetValue | Yes | The target value that defines success. |
StartingValue | No | The baseline value. Below TargetValue for Increase, above it for Decrease. |
Prefix | No | Symbol shown before the value (e.g. $). Max 8 chars. |
Suffix | No | Symbol shown after the value (e.g. %). Max 8 chars. |
TargetDirection | Yes | Increase 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.
| Column | Required | Notes |
|---|---|---|
Type | Yes | Program or Portfolio. |
Id | Yes | The program or portfolio this row closes, by id, per Type. |
Status | Yes | Programs: Completed or Canceled. Portfolios: Closed or Archived. |
EndDate | Conditional | Required for portfolio rows (becomes the portfolio's end date); ignored for program rows. |
Related
- Data generation tool — generates all of these CSVs and seeds them for you, in the correct order.
- Organization Bulk Import — the equivalent imports for employees, teams, and staffing (run before PPM, since PPM references people and teams).
- Planning Bulk Import — planning interval objectives and risks.
- Product Management Bulk Import — the product catalog.
- PPM Lifecycle Rules — the status constraints the imports respect.
- Portfolios & Programs, Projects, Strategic Initiatives — the features these imports populate.