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


# Wire format

This guide covers the conventions every raw Planning Engine call shares: how you pass JSON in, how you read the envelope that comes back, and how ids, dates, numbers, `null` and unknown fields are read. Reference pages for each export link here instead of repeating these rules.

## Strings in, strings out

The raw module has two exports, the [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) class and the [`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>) function. Every argument is JSON text and every return value is JSON text. Nothing native crosses the boundary: no objects, no `Date`s, no exceptions for bad input.

Each call follows the same four steps:

1. Serialize the request and options with `JSON.stringify`.
2. Call the export.
3. Parse the returned string with `JSON.parse`.
4. Check `ok`, then read `result` or `error`.

The `plen` CLI speaks the same wire format, byte for byte.

Here's the full loop in a browser or ES module host, with a stateful project:

```js
import init, { JsonProject } from "./pkg/plen_wasm.js";

await init();

const data = {
  config: { use_calendars: false },
  tasks: [
    { id: "a", start_date: "2026-01-01", duration: 2 },
    { id: "b", start_date: "2026-01-01", duration: 1 },
  ],
  links: [{ id: "ab", source: "a", target: "b" }],
};

const project = new JsonProject("{}");
try {
  const text = project.solve(JSON.stringify(data), JSON.stringify({}));
  const envelope = JSON.parse(text);

  if (envelope.ok) {
    for (const update of envelope.result.updates) {
      console.log(update.id, update.start_date, update.end_date);
    }
  } else {
    const { kind, code, message, path } = envelope.error;
    console.error(kind, code, message, path);
  }
} finally {
  project.free();
}
```

In Node, the CommonJS build loads synchronously, so there's no [`init()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md>). The stateless `schedule` export needs no handle to free:

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

const data = {
  tasks: [{ id: 1, start_date: "2026-01-05", duration: 3 }],
};

const envelope = JSON.parse(schedule(JSON.stringify(data), "{}"));
if (!envelope.ok) {
  throw new Error(`${envelope.error.kind}: ${envelope.error.message}`);
}
console.log(envelope.result.updates);
```

Every export is synchronous. Only module instantiation is async in the browser build. How to load each build, and how to create, clone and free handles, is in [Module and handles](<https://docs.dhtmlx.com/planning-engine/wasm/module-and-handles.md>).

## Envelope

Every export returns one of two shapes.

Success wraps the payload in `result`:

```json
{
  "ok": true,
  "result": {
    "baselineId": "<opaque token>",
    "updates": [
      { "id": "b", "kind": "task", "start_date": "2026-01-03 00:00", "end_date": "2026-01-04 00:00" }
    ],
    "cycles": [],
    "conflicts": [],
    "notes": [],
    "converged": true
  }
}
```

That's the response to the browser example above: task `b` moved after `a`, and `a` didn't move, so only `b` is listed. The `result` payload depends on the export. Each reference page names its payload type.

Failure carries an `error` object with four fields, always all present:

```json
{
  "ok": false,
  "error": {
    "kind": "parse",
    "code": null,
    "message": "missing field `tasks`",
    "path": null
  }
}
```

| Field | Type | Meaning |
|---|---|---|
| `kind` | `"parse"` \| `"validation"` \| `"project"` | The error class. Branch on this. |
| `code` | string \| `null` | Set only for `project` errors. |
| `message` | string | Human-readable text. Don't parse it. |
| `path` | string \| `null` | The input field at fault, such as `tasks[3].constraint_date`, when the engine knows it. |

[`baselineId`](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/reading-results.md#baseline-token>) values are opaque tokens. Don't compare them across runs: the same data can get a new token on a repeat run.

## Error table

Errors come at two levels. Most failures arrive in the envelope. A short list of failures throws in the host instead.

### Errors in the envelope

| `kind` | `code` | `path` | When |
|---|---|---|---|
| `parse` | `null` | `null` | The text isn't valid JSON, a value has the wrong JSON type, a required field is missing or `null`, an unknown field appears where unknown fields are rejected, or a record is an array instead of an object. |
| `validation` | `null` | `null` | The decoder rejects a value: an invalid date, an unknown enum string, a bad link type, a non-integer or out-of-range integer, `days` without exactly 7 entries, `customWeeks`, `level: true`. |
| `validation` | `null` | field path | The engine rejects the data or options as a whole: duplicate ids, inconsistent dates, a hierarchy cycle, an unknown id in a scope list, and similar checks. `path` points at the field, e.g. `tasks[0].start_date` or `slots[2].allowed[0]`. |
| `project` | `"no-baseline"` | `null` | The call needs solved dates, and the project has never been solved or was edited by [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) since the last solve. |
| `project` | `"stale-baseline"` | `null` | A `baselineId` you passed doesn't name the current baseline. |
| `project` | `"needs-resync"` | `null` | An earlier `apply` rejected an operation, and no [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) with full data has followed. |

For `project` errors, `message` holds the same string as `code`.

An input error before solving leaves the project as it was: the previous accepted state and cached result are kept.

:::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`, this means no operation from that change set is applied.
:::

Some outcomes look like failures but come back with `ok: true`, as data in the result: scheduling conflicts, dependency cycles, failure to converge, rejected `apply` operations, per-candidate [`evaluate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md>) failures, and unfilled or invalid assignment [slots](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/slot.md>). Check the result fields for them.

### Host-level failures

| Failure | What the host sees |
|---|---|
| Bad options in [`new JsonProject(options_json)`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#creating>) | The constructor throws a **string**: the error envelope text. It's not an `Error` object. |
| A method called on a [freed](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#free>) handle | A thrown error from the generated bindings. |
| The `.wasm` can't be fetched or instantiated | [`init()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md>) rejects. Calling exports before `init()` resolves fails too. |
| An internal engine failure | A wasm trap, surfaced as `WebAssembly.RuntimeError`. |

The constructor is the only export that throws for bad input. Parse what it throws:

```js
import init, { JsonProject } from "./pkg/plen_wasm.js";

await init();

let project;
try {
  project = new JsonProject(JSON.stringify({ validation: "lenient" }));
} catch (thrown) {
  const envelope = JSON.parse(thrown);
  console.error(envelope.error.kind, envelope.error.message);
  // validation unknown validation mode
}
```

Recovery for each error kind is in [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>).

## Field spelling

Two naming styles meet on the wire, and which one applies depends on what the object is:

| Object | Style | Examples |
|---|---|---|
| Project data: `config`, calendars, tasks, links, resources, assignments | snake_case | `start_date`, `duration_unit`, `calendar_id`, `apply_constraints` |
| Change-set records (`add` and `update` entries) | snake_case | `leveling_delay`, `constraint_type` |
| Result update entries (`updates[]`) | snake_case | `start_date`, `end_date`, `leveling_delay` |
| Call options | camelCase | `includeUnchanged`, `baselineId`, `clearFirst` |
| [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>)`.baselineId` | camelCase | `baselineId` |
| Envelope and every other result field | camelCase | `criticalPath`, `needsResync`, `removedTasks` |

Task and link types are both spelled `type`.

Update entries reuse the task-data names on purpose. A result entry decodes as a valid task update, so you can send it back in `tasks.update` as is. Its extra `kind` and `reason` fields are ignored there.

Getting the style wrong fails in different ways:

- In call options, a wrongly spelled field is an unknown field, so the call fails with `parse`. `{"clear_first": true}` in [evaluate options](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md#evaluateoptions>) is rejected.
- In project data and change-set records, unknown fields are ignored. A task with `"startDate"` decodes as a task with no start date, and the error (if any) comes later from validation.

## Ids, dates and numbers

### Ids

An [id](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#id>) is a JSON number or a string. Ids are compared like JavaScript object keys, so `1` and `"1"` name the same task. Numeric ids are double-precision floats, like JavaScript numbers; send ids beyond `Number.MAX_SAFE_INTEGER` as strings. Whole-number ids come back as JSON integers.

A task `parent` of `""`, or equal to [`config`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>)`.root_id` (default `0`), means the task has no parent.

### Dates

Dates are strings: a naive local [date-time](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#datevalue>) at minute precision. There's no timezone, no daylight saving and no "today". Convert to local wall-clock time before you send dates in.

| Direction | Accepted forms |
|---|---|
| Input | `YYYY-MM-DD`, `YYYY-MM-DD HH:mm`, `YYYY-MM-DDTHH:mm`, each with optional `:ss`. Seconds are dropped. |
| Output | Always `YYYY-MM-DD HH:mm`. |

So `"2026-01-01T08:00:59"` comes back as `"2026-01-01 08:00"`. An unparsable date fails with `validation`, message `invalid date`, `path: null`.

Calendar exception keys in [`worktime`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/calendar-data.md#worktime-fields>)`.dates` are `YYYY-MM-DD` only. Hour ranges in `worktime.hours` are `"H:MM-H:MM"` strings, `"24:00"` is the only accepted hour-24 value, and a range that ends before it starts crosses midnight.

The generated type files type dates as `string | Date`. `Date` objects are a convenience of the JS glue only; on the raw surface you send strings.

### Numbers

Numbers are plain JSON numbers, and fractions are allowed where the field allows them. Whole-valued results come back as integers: `5`, not `5.0`.

Some fields take integers only:

- `priority` - integer 0..1000.
- `max_iterations`, [requirement](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-slots.md#requirement>) `count`, [suggest](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-suggest-assignments.md#suggestoptions>) `passes` - non-negative integers. `8.0` is accepted; `8.5` fails with `validation`.

JSON can't carry `NaN` or `Infinity`. `JSON.stringify` turns them into `null`, and `null` then reads as "use the default" or "clear the field" (see the next section). Check that numbers are finite before you serialize.

### Enum values

Enum fields are strings. A few accept extra input forms:

| Field | Accepted | Notes |
|---|---|---|
| [Link `type`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#link-type>) | `"fs"`, `"ss"`, `"ff"`, `"sf"` in any case; `0`-`3`; `"0"`-`"3"` | `0` = FS, `1` = SS, `2` = FF, `3` = SF |
| [Task `type`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#task-type>) | `"task"`, `"summary"`, `"project"`, `"milestone"` | `"project"` is an input alias of `"summary"`. Output says `"summary"`. |
| [`duration_unit`, `lag_unit`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#timeunit>) | `"minute"`, `"hour"`, `"day"` | `week`, `month`, `year` are rejected. |

Any other string fails with `validation` and no `path`. The full lists of enum values are in [Common types](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md>).

## Omitted, null and unknown fields

### Input: omitted and `null`

In project data and in options, an omitted field and a `null` field mean the same thing: use the default. `{"includeUnchanged": null}` is the same as `{}`. A `null` [`config`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>) gives the default config.

Update records (`update` entries of a change set) are the exception. There, omitted and `null` differ:

| In an update entry | Effect |
|---|---|
| Field omitted | Leave the field unchanged. |
| Field set to `null` | Clear an optional field, or restore a scalar default. |
| Field set to a value | Set it. |

```json
{
  "tasks": {
    "update": [
      { "id": 1, "start_date": null, "constraint_type": "", "progress": 0.5 }
    ]
  }
}
```

This update clears the start date and the constraint, sets `progress`, and leaves every other field of task `1` alone. For `constraint_type`, an empty string also clears. `pauses: null` clears the list, and a new `pauses` list replaces the whole list. The full list of update fields is on [TaskUpdate](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/task-update.md>).

### Input: unknown fields

Whether an unknown field is ignored or rejected depends on the object:

| Ignored | Rejected with `parse` |
|---|---|
| Project data records: tasks, links, resources, assignments, calendars | Every call options object |
| [`config`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>) | [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) itself, and its `add` / `update` / `remove` buckets |
| Change-set `add` and `update` entries | [`Candidate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md#candidate>) |
| | [Slots](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/slot.md>), [pool, workers, skills and requirements](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-slots.md>) |

Records ignore unknown fields so you can pass UI data with its own extras (`text`, `open`, custom columns) and feed result entries back as updates. Options and change-set containers are strict, so a typo like `{"tasks": {"rename": []}}` or `{"maxRounds": 5}` fails loudly instead of being dropped.

### Output: absent fields

Optional result fields are omitted when they don't apply, not set to `null`. `baselineId` is missing when there's no baseline. An update entry lists only the fields that changed. [`analytics`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#analytics>) appears only when you asked for it.

A few result fields use `null` as a real value, and their reference pages say so: for example `fromResource: null` in a [suggest step](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-suggest-assignments.md#suggeststep>) means the slot was filled, not moved.

## Required fields

A required field must be present and non-`null`. A missing or `null` required field fails the call with `parse` and no `path`. For example, `{"tasks": null}` as project data is a parse error.

The main required fields:

| Object | Required |
|---|---|
| [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/project-data.md>) | `tasks` |
| [Task](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/task-data.md>), [resource](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#resourcedata>) and [calendar](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/calendar-data.md>) records | `id` |
| [Link](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/link-data.md>) in project data | `id`, `source`, `target` |
| Link in [`links.add`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md#linkadd>) | `source`, `target` (`id` is generated when omitted) |
| [Assignment](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/assignment-data.md>) | `task`, `resource`, `value` |
| [Update entries](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/task-update.md>) | `id` |
| [`Subtree`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#subtree>) | `task` |
| [Profile query](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#profilequery>), [`loadProfile` options](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md#loadprofileoptions>) | `from`, `to` |
| [`Candidate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md#candidate>) | `id`, `changes` |
| [Slot](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/slot.md>) | `id`, `task`, `allowed` |
| [Requirement](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-slots.md#requirement>) | `id`, `task`, `matches` |

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

Assignments also accept `task_id` and `resource_id` as aliases of `task` and `resource`. When both spellings are present, the primary name wins. In an assignment update, setting `task` or `resource` to `null` is rejected: a required endpoint can't be cleared.

Records must be JSON objects. A positional array such as `[1, "2026-01-05", 3]` in place of a task is a parse error.

Required fields only cover the shape. The engine checks the content next: for example, a leaf task needs at least two of `start_date`, `end_date` and `duration`, and an entirely undated project is rejected. Those checks fail with `validation` and a `path`. The rules are in [Project data](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/project-data.md>).

For every request and response type, field by field, see the [types overview](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/overview.md>).
