[Canonical page](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/project-data/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Project data

This guide shows how to describe a plan as JSON for the Planning Engine WebAssembly module: tasks, links, ids, dates, durations, and the config switches that change how the plan is scheduled. It also lists what strict validation rejects, so your first payload is valid.

## How project data works

A plan goes in as one [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/project-data.md>) JSON object with six keys: `config`, `calendars`, `tasks`, `links`, `resources` and `assignments`. Only `tasks` is required. The lists default to empty, and every `config` field has its own default.

:::note
`tasks` is required in `ProjectData`; send `"tasks": []` for an empty project.
:::

`ProjectData` is an import format, not stored state. You pass it as the `data_json` string to [`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>), [`JsonProject.solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) or [`JsonProject.analyze`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-analyze.md>). The engine validates it and builds its own normalized model. It never sends the snapshot back. Results list only the tasks that changed, as sparse [`updates`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#updates>), and you write those into your own model.

Field names are snake_case (`start_date`, `duration`, `constraint_type`, `auto_scheduling`). Project data, config and the `updates` entries in results use snake_case. Call options and the other result fields use camelCase (`includeUnchanged`, `baselineId`). The task type and the link type are both spelled `type`.

A few rules hold everywhere:

- An id is a JSON number or string. Ids compare like JavaScript object keys, so task `1` and task `"1"` are the same task.
- A date is a string: a naive local date-time at minute precision. There's no time zone, no DST and no "today".
- `duration` and `lag` are working time, measured on calendars. `leveling_delay` and `pauses` are elapsed wall-clock time.
- Each record must be a JSON object. Unknown fields in records and in `config` are ignored, so you can send tasks with `text`, `color` and other UI fields as they are.
- `null` on a record field means "use the default".
- Strict validation (the default) rejects ambiguous input with a `validation` error envelope and a `path` to the field instead of guessing. `{"validation": "repair"}` on a [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#projectoptions>) fixes what it can and reports each fix.

How envelopes, dates and numbers are encoded is covered in [Wire format](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md>). For every field and its default, see [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/project-data.md>), [`TaskData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/task-data.md>) and [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>).

## Describing a project

### Tasks

A leaf task needs two of `start_date`, `end_date` and `duration`. The engine derives the third. If you send all three, they must agree on the task's calendar.

```json
{
  "tasks": [
    { "id": "phase", "type": "summary" },
    { "id": "design", "parent": "phase", "start_date": "2026-08-10 08:00", "duration": 3 },
    { "id": "build", "parent": "phase", "start_date": "2026-08-10 08:00", "duration": 5 },
    { "id": "release", "type": "milestone", "start_date": "2026-08-10 08:00" }
  ],
  "links": [
    { "id": "l1", "source": "design", "target": "build" },
    { "id": "l2", "source": "build", "target": "release" }
  ]
}
```

Send it with the one-shot [`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>) export. This example uses the Node build:

```js
const { schedule } = require("./pkg-node/plen_wasm.js");

const data = {
  tasks: [
    { id: "phase", type: "summary" },
    { id: "design", parent: "phase", start_date: "2026-08-10 08:00", duration: 3 },
    { id: "build", parent: "phase", start_date: "2026-08-10 08:00", duration: 5 },
    { id: "release", type: "milestone", start_date: "2026-08-10 08:00" },
  ],
  links: [
    { id: "l1", source: "design", target: "build" },
    { id: "l2", source: "build", target: "release" },
  ],
};

const envelope = JSON.parse(schedule(JSON.stringify(data), "{}"));
if (envelope.ok) {
  for (const u of envelope.result.updates) {
    console.log(u.id, u.kind, u.start_date ?? "", u.end_date ?? "");
  }
} else {
  const { kind, message, path } = envelope.error;
  console.error(`${kind} at ${path ?? "-"}: ${message}`);
}
```

The `updates` list in the result:

```json
[
  { "end_date": "2026-08-20 08:00", "id": "build", "kind": "task", "start_date": "2026-08-13 08:00" },
  { "duration": 8, "end_date": "2026-08-20 08:00", "id": "phase", "kind": "summary" },
  { "end_date": "2026-08-20 08:00", "id": "release", "kind": "milestone", "start_date": "2026-08-20 08:00" }
]
```

The links push `build` after `design` and `release` after `build`. The summary `phase` has no dates of its own, so its span is rolled up from `design` and `build`. Its end moved, so it's reported too. Each entry carries only the fields that changed: `phase` still starts on Monday, so it has no `start_date`. `design` didn't move and isn't listed.

Each [task type](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#task-type>) has its own date rule:

- `"task"` (the default) is a leaf: two of start, end and duration.
- `"summary"` takes both dates or neither. Its span is derived, so a summary with `start_date` + `duration` is rejected. A summary with no dates needs at least one dated child. `"project"` is accepted as an alias on input, and results say `"summary"`.
- `"milestone"` needs `start_date` or `end_date`. Its duration is 0.

`parent` links a task to its summary. A missing `parent`, `null`, `""` and a value equal to `config.root_id` (default `0`) all mean "no parent". A parent that doesn't exist, or a `parent` chain that loops back on itself, is a validation error.

The rest of [`TaskData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/task-data.md>) covers constraints (`constraint_type`, `constraint_date`), calendars (`calendar_id`), and leveling inputs (`priority`, `leveling_delay`, `pauses`). See [Links, constraints and summaries](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/links-constraints-summaries.md>), [Working time and calendars](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/working-time-calendars.md>) and [Leveling resources](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/leveling-resources.md>).

### Ids and dates

An [id](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#id>) is a number or a string, and the engine compares ids by their JavaScript key. A number and its string form are the same id, so a link can name task `1` as `"1"`:

```json
{
  "tasks": [
    { "id": 1, "start_date": "2026-08-10T09:30:45", "duration": 1 },
    { "id": "2", "start_date": "2026-08-10 09:30", "duration": 1 }
  ],
  "links": [{ "id": 1, "source": "1", "target": 2 }]
}
```

```json
[{ "end_date": "2026-08-12 09:30", "id": "2", "kind": "task", "start_date": "2026-08-11 09:30" }]
```

Results echo each id in the form you sent it, so task `"2"` comes back as a string. The same rule causes collisions when you merge sources. Task `1` from one system and task `"1"` from another are duplicates, and strict validation rejects the second one at `tasks[1].id`. Numeric ids are JavaScript numbers, so integers past 2^53 lose precision.

A [date](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#datevalue>) string can be `YYYY-MM-DD`, `YYYY-MM-DD HH:mm` or `YYYY-MM-DDTHH:mm`, with optional seconds. Seconds are dropped, so `09:30:45` above is read as `09:30`. Results always use `YYYY-MM-DD HH:mm`. An impossible date such as `2026-02-30` fails the whole call with a `validation` error whose `path` is `null`.

Dates carry no time zone. Convert zoned timestamps to the plan's local time before you build the JSON, and never pass `Date` objects: `JSON.stringify` turns them into UTC ISO strings. The engine never reads the clock, so an entirely undated project is rejected instead of being placed at "today".

### Durations and units

`duration` is working time in a [unit](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#timeunit>): `"minute"`, `"hour"` or `"day"`. `config.duration_unit` sets the unit for the whole project (default `"day"`). A task's own `duration_unit` overrides it for that task.

```json
{
  "config": { "duration_unit": "hour" },
  "tasks": [
    { "id": "review", "start_date": "2026-08-10 08:00", "duration": 12 },
    { "id": "sync", "start_date": "2026-08-10 08:00", "duration": 90, "duration_unit": "minute" }
  ]
}
```

With `{"includeUnchanged": true}` as the options, every task comes back with its dates:

```json
[
  { "duration": 12, "end_date": "2026-08-11 12:00", "id": "review", "kind": "task", "start_date": "2026-08-10 08:00" },
  { "duration": 90, "end_date": "2026-08-10 09:30", "id": "sync", "kind": "task", "start_date": "2026-08-10 08:00" }
]
```

The default calendar works 08:00-12:00 and 13:00-17:00, Monday to Friday. Twelve working hours from Monday 08:00 fill Monday and end at Tuesday 12:00. See [Load all task dates](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/load-all-task-dates.md>) for more on `includeUnchanged`.

Durations can be fractional, so `1.5` days or `0.25` hours are valid. A negative duration is rejected. Week, month and year units aren't supported: `"week"` fails the call with a `validation` error.

Day units count whole working days. From a midnight anchor, Tuesday 00:00 + 4 days ends at Saturday 00:00, which is the close of Friday. From a non-midnight anchor, days are counted to the same time of day. With a sparse calendar this can skip working hours: on a Mon-Wed 9:00-13:00 calendar, a 3-day task from Monday 9:00 ends the next Monday 9:00. If that matters, use hour units.

Two task fields use elapsed time instead of working time: `leveling_delay` and `pauses`. Both are in the task's `duration_unit`, where a day is 24 hours, and both are rounded to whole minutes on use. The leveler writes them. Read [Leveling resources](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/leveling-resources.md>) before you set them by hand.

### Links

A [`LinkData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/link-data.md>) record connects a `source` task to a `target` task. [`type`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#link-type>) is `"fs"` (finish-to-start, the default), `"ss"`, `"ff"` or `"sf"`. The numeric codes `0` to `3` also work, as numbers or strings. `lag` is working time added along the link, in `config.duration_unit` unless the link sets its own `lag_unit`.

```json
{
  "tasks": [
    { "id": "a", "start_date": "2026-08-10 08:00", "duration": 2 },
    { "id": "b", "start_date": "2026-08-10 08:00", "duration": 2 },
    { "id": "c", "start_date": "2026-08-10 08:00", "duration": 2 }
  ],
  "links": [
    { "id": "ss", "source": "a", "target": "b", "type": "ss", "lag": 1 },
    { "id": "ff", "source": "b", "target": "c", "type": 2, "lag": 4, "lag_unit": "hour" },
    { "id": "stray", "source": "c", "target": "missing" }
  ]
}
```

```json
{
  "notes": [{ "kind": "dropped-link", "link": "stray", "reason": "unknown target task \"missing\"" }],
  "updates": [
    { "end_date": "2026-08-13 08:00", "id": "b", "kind": "task", "start_date": "2026-08-11 08:00" },
    { "end_date": "2026-08-13 12:00", "id": "c", "kind": "task", "start_date": "2026-08-11 12:00" }
  ]
}
```

`b` starts one working day after `a` starts. `c` (type `2`, finish-to-finish) finishes four working hours after `b` finishes.

Some links can't apply. The engine drops them and reports a `dropped-link` [note](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#note>) with the reason instead of failing:

- the source or target task doesn't exist;
- the source or target task is unscheduled;
- the link joins a summary to one of its own descendants, and `config.descendant_links` is `false`.

A duplicate link id is a validation error. A link from a task to itself is kept and reported in [`cycles`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#cycle>) as a one-task cycle. JSON can't carry `NaN` or `Infinity`: `JSON.stringify` writes them as `null`, and a `null` lag reads as `0`. Check that numbers are finite before you serialize. How each link type moves its target, and how links attach to summaries, is in [Links, constraints and summaries](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/links-constraints-summaries.md>).

### Fixed, unscheduled and completed tasks

Three task states keep a task where it is:

- `"auto_scheduling": false` makes the task fixed. The planner never moves it, but links from it still push its successors.
- `"unscheduled": true` takes the task out of the plan. It isn't scheduled or reported, and it doesn't need dates. Its descendants go with it. Links that touch it are dropped with a note.
- With `config.use_progress` on, a task with `"progress": 1` is completed and stays put.

```json
{
  "config": { "use_progress": true },
  "tasks": [
    { "id": "spec", "start_date": "2026-08-10 08:00", "duration": 1 },
    { "id": "done", "start_date": "2026-08-10 08:00", "duration": 2, "progress": 1 },
    { "id": "pinned", "start_date": "2026-08-10 08:00", "duration": 1, "auto_scheduling": false },
    { "id": "next", "start_date": "2026-08-10 08:00", "duration": 1 },
    { "id": "later", "unscheduled": true }
  ],
  "links": [
    { "id": 1, "source": "spec", "target": "done" },
    { "id": 2, "source": "spec", "target": "pinned" },
    { "id": 3, "source": "pinned", "target": "next" },
    { "id": 4, "source": "next", "target": "later" }
  ]
}
```

```json
{
  "notes": [
    { "kind": "dropped-task", "reason": "unscheduled", "task": "later" },
    { "kind": "dropped-link", "link": 4, "reason": "target task \"later\" is unscheduled" }
  ],
  "updates": [
    { "end_date": "2026-08-12 08:00", "id": "next", "kind": "task", "start_date": "2026-08-11 08:00" }
  ]
}
```

`spec` would push `done` and `pinned`, but neither moves. `pinned` still pushes `next`. `later` and its link drop out with notes.

`progress` must be between 0 and 1. A completed task also stays in place inside a summary that moves as a block. In analytics, completed tasks have zero slack and are never critical.

### Scheduling config

`config` holds the project-wide switches. Send only the fields you want to change:

```json
{
  "config": {
    "apply_constraints": true,
    "gap_behavior": "compress",
    "duration_unit": "hour",
    "project_start": "2026-08-10 08:00",
    "calendar_mode": "task",
    "root_id": -1
  },
  "tasks": [{ "id": 1, "parent": -1, "start_date": "2026-08-12 08:00", "duration": 4 }]
}
```

```json
[{ "end_date": "2026-08-10 12:00", "id": 1, "kind": "task", "start_date": "2026-08-10 08:00" }]
```

`"compress"` pulls the task from Wednesday back to `project_start`. `parent: -1` equals `root_id`, so task `1` has no parent.

The switches you'll change most often:

| Field | Default | Effect |
|---|---|---|
| `apply_constraints` | `false` | Turns on the eight constraint types. Off, constraints are ignored, including `mso` pins |
| `schedule_from_end` | `false` | Backward (ALAP) planning. [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) refuses such projects |
| `gap_behavior` | `"preserve"` | `"preserve"` keeps tasks at their own dates when links allow; `"compress"` pulls every task to its earliest position |
| `move_projects` | `true` | Summaries move as a block; `false` schedules their leaves one by one |
| `use_progress` | `false` | Tasks with `progress` 1 are completed and fixed |
| `project_start` | absent | Lower bound for the plan, and the date source for undated tasks in repair mode |
| `project_end` | absent | Upper bound, used by backward projects |
| `duration_unit` | `"day"` | Unit of `duration` and `lag` |
| `use_calendars` | `true` | `false` makes every calendar flat 24/7 time |
| `calendar_mode` | `"resource"` | Which calendars place the work; `"task"` / `"task-inherited"` use task calendars only |
| `max_iterations` | `8` | Planner pass limit; must be at least 1 |
| `root_id` | `0` | The `parent` value that means "no parent" |

`project_constraint`, `group_mode`, `descendant_links`, `strict_calendar` and `default_resource_capacity` cover rarer cases. See [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>) for all of them.

`root_id` exists for data whose virtual root isn't `0`. The engine strips it on import, so stored tasks never point at the root. With a non-zero `root_id`, a task with id `0` is an ordinary parent.

Left out (or `null`), `use_calendars` means Planning Engine schedules on calendars. Set it explicitly when your data should run on flat time.

### Validation rules

Strict validation, the default, rejects input that would need coercing or guessing. The call returns an error envelope instead of a result. A live [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) keeps its previous state and cached result.

```js
const { schedule } = require("./pkg-node/plen_wasm.js");

const data = {
  tasks: [
    { id: "ok", start_date: "2026-08-10 08:00", duration: 1 },
    { id: "broken", duration: 2 }, // only one of start / end / duration
  ],
};

const envelope = JSON.parse(schedule(JSON.stringify(data), "{}"));
if (!envelope.ok) {
  const { kind, message, path } = envelope.error;
  console.log(`${kind} at ${path ?? "-"}: ${message}`);
  // validation at tasks[1].start_date: task needs two of start_date / end_date / duration
}
```

The engine checks these rules after decoding and reports the field's path:

| Input | Error path |
|---|---|
| Duplicate task, link, resource or calendar id | `tasks[i].id`, `links[i].id`, `resources[i].id`, `calendars[i]` |
| A leaf with fewer than two of start, end and duration | `tasks[i].start_date` |
| Start, end and duration that don't agree | `tasks[i].duration` |
| `end_date` before `start_date` | `tasks[i].end_date` |
| A summary with only one date, or start + duration | `tasks[i].start_date` |
| A summary with no dates and no dated children | `tasks[i]` |
| A milestone with no date | `tasks[i].start_date` |
| A negative duration | `tasks[i].duration` |
| A dated constraint without `constraint_date` | `tasks[i].constraint_date` |
| An unknown `parent`, or a cycle in the hierarchy | `tasks[i].parent` |
| `progress` outside 0..1, negative `leveling_delay` | `tasks[i].progress`, `tasks[i].leveling_delay` |
| Unsorted, overlapping or invalid `pauses` | `tasks[i].pauses[j]` |
| An unknown task `calendar_id` | `tasks[i].calendar_id` |
| A calendar with no working time | `calendars[i]` |
| `max_iterations: 0` | `config.max_iterations` |

Some values fail while the JSON is decoded, before any of those checks. They return a `validation` error with `path: null`: an invalid date, an unknown `type`, link type or enum string, a `priority` that isn't a whole number from 0 to 1000, a malformed hour range, `"week"`, `"month"` or `"year"` units, and `customWeeks` calendars. A record that isn't a JSON object, a wrong JSON type, or a missing required field such as `tasks` or a task `id` is a `parse` error.

:::note
A `priority` outside 0..1000 or a malformed hour range fails the whole call with a `validation` error that has no `path`. In [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>), this means no operation from that change set is applied.
:::

Resource and assignment rules (capacity, `value`, slices, calendars with no shared working time) are in [Resources and assignments](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/resources-and-assignments.md>).

Some things aren't errors. Links that can't apply become `dropped-link` notes, unscheduled tasks become `dropped-task` notes, and dependency cycles are reported in [`cycles`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#cycle>). All of them come back in an `ok: true` result.

When the data comes from a UI that saves messy records, switch to repair mode instead of fixing the input yourself. Repair is a [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#projectoptions>) option:

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

const data = {
  tasks: [
    { id: "a", start_date: "2026-08-10 08:00", duration: 1 },
    // Same id again: in repair mode the last record wins.
    { id: "a", start_date: "2026-08-10 08:00", duration: 3 },
  ],
};

const project = new JsonProject('{"validation":"repair"}');
try {
  const envelope = JSON.parse(project.solve(JSON.stringify(data), "{}"));
  if (envelope.ok) {
    for (const note of envelope.result.notes) {
      if (note.kind === "repaired") console.log(`${note.path}: ${note.message}`);
    }
    // tasks[1].id: duplicate id replaced the earlier record, retaining its position
  } else {
    console.error(envelope.error);
  }
} finally {
  project.free();
}
```

Repair deduplicates ids (the last record wins and keeps the first position), derives missing dates and durations, lets dates win in an inconsistent triple, and clears dated constraints without a date. Each fix shows up as a `repaired` note with its path. A task with no dates gets `config.project_start`, or the earliest date of another task. When neither exists, as in a fully undated project, repair fails at `tasks[i].start_date`. The `JsonProject` [constructor](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#creating>) throws the error envelope as a string if the options are invalid, for example an unknown `validation` mode. See [Accept messy input](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/accept-messy-input.md>) and [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>).
