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


# Quick start

By the end of this page, you'll have a Node.js script that schedules two linked tasks through the raw WebAssembly exports, and a live project that reschedules them when one changes.

If you build a browser or Node.js app and want a `Project` class with typed errors, start at [JavaScript usage](<https://docs.dhtmlx.com/planning-engine/wasm/javascript.md>) instead. This page uses the raw surface: JSON strings in, JSON strings out.

## Requirements

- Node.js 18 or later for the examples on this page, or a modern browser with WebAssembly support.
- No runtime dependencies.

## Install

The package comes from a private npm registry at `git.dhtmlx.com`. With your license or trial you received a username and a password for one of two editions:

| Edition | Registry URL | Package |
| --- | --- | --- |
| Full | `https://git.dhtmlx.com/api/packages/plen/npm/` | `@dhx/plen-wasm` |
| Trial | `https://git.dhtmlx.com/api/packages/plen-trial/npm/` | `@dhx/plen-wasm-trial` |

The trial is the full engine with one limit: a project can have at most 25 tasks. The exports are the same.

Each account sees only its own edition. Trial credentials can't install `@dhx/plen-wasm`, and full credentials only see the `plen` registry.

### Configure access to the registry

The examples use the full registry. For the trial, replace `plen` with `plen-trial` in the URL, on both lines.

**Option A: access token (recommended).** Sign in at <https://git.dhtmlx.com> with your username and password, open *Settings → Applications*, and create an access token with the scope **package: Read**. Then create an `.npmrc` next to your `package.json`:

```ini
@dhx:registry=https://git.dhtmlx.com/api/packages/plen/npm/
//git.dhtmlx.com/api/packages/plen/npm/:_authToken=${DHTMLX_NPM_TOKEN}
```

The token comes from an environment variable, so the file can go into source control. Set the variable in your shell:

```bash
# bash / zsh
export DHTMLX_NPM_TOKEN=your-token
```

```powershell
# PowerShell
$env:DHTMLX_NPM_TOKEN = "your-token"
```

npm fails when the variable is missing, so set it in every environment that installs: as a secret on your CI server, and in Docker builds. npm, pnpm, Bun and Yarn 1 read `.npmrc`.

**Option B: username and password.** Pass the base64-encoded `username:password` pair instead of a token. This doesn't work for accounts with two-factor authentication.

```ini
@dhx:registry=https://git.dhtmlx.com/api/packages/plen/npm/
//git.dhtmlx.com/api/packages/plen/npm/:_auth=${DHTMLX_NPM_AUTH}
```

```bash
export DHTMLX_NPM_AUTH=$(printf '%s' 'your-username:your-password' | base64)
```

**Yarn 2 and later** read `.yarnrc.yml` instead of `.npmrc`:

```yaml
npmScopes:
  dhx:
    npmRegistryServer: "https://git.dhtmlx.com/api/packages/plen/npm/"
    npmAuthToken: "${DHTMLX_NPM_TOKEN}"
```

**Already using npm.dhtmlx.com.** The `@dhx` scope can point to one registry only. If your project already installs DHTMLX packages such as `@dhx/gantt` from `https://npm.dhtmlx.com/`, keep that mapping. The planning engine packages are published there too, and your existing login installs them. If it can't see them, contact DHTMLX support.

### Install the package

Full edition:

```bash
npm install @dhx/plen-wasm
```

Trial edition, installed under the full name with an npm alias, so the path doesn't change when you move to the full edition:

```bash
npm install @dhx/plen-wasm@npm:@dhx/plen-wasm-trial
```

With pnpm or Yarn, use `pnpm add` or `yarn add` with the same argument.

To move from the trial to the full edition later, change `plen-trial` to `plen` on both lines of `.npmrc`, use a token or the username and password of your full-edition account, and run `npm install @dhx/plen-wasm@^1`.

### Find the raw files

The package holds two builds of the module, each with the `.wasm` binary and the generated JS bindings:

- `pkg-node/`: the Node.js CommonJS module.
- `pkg/`: the browser ES module.

The package's `exports` map points only to the [JS glue](<https://docs.dhtmlx.com/planning-engine/wasm/javascript.md>), so `require("@dhx/plen-wasm/pkg-node/plen_wasm.js")` fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`. For raw use, load the generated files by path. The examples below assume you copied `pkg-node/` (or `pkg/`) next to your script:

```bash
cp -r node_modules/@dhx/plen-wasm/pkg-node .
```

A relative path into `node_modules` works too, such as `require("./node_modules/@dhx/plen-wasm/pkg-node/plen_wasm.js")`.

### If the install fails

- **`E401` or `Unable to authenticate`:** the token or password is wrong, or the variable isn't set in the shell that runs `npm`. Check that the token has the scope **package: Read**.
- **`E404 Not Found`:** the registry and the package don't match. Trial credentials see only `@dhx/plen-wasm-trial` on the `plen-trial` registry. Also check that the URL ends in `/npm/` on both lines.
- **Requests go to `npm.dhtmlx.com` or `registry.npmjs.org`:** another `@dhx:registry` line wins. Run `npm config get @dhx:registry` in the project folder. A project `.npmrc` overrides your user config.
- **The lockfile still points at the old registry after a change:** the lockfile stores full tarball URLs. Delete `node_modules` and the package's lockfile entry, then install again.

## Load the module

The two targets load differently.

In Node.js, `require` loads and instantiates the module in one step. It's ready right away:

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

In the browser, import the ES module and await its default export, [`init`](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md>), once before the first call:

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

await init();
```

`init()` fetches `plen_wasm_bg.wasm` from the same folder as the JS file, so serve both files over HTTP. If you call an export before `init()` resolves, the call fails.

That's the only async step. Every export after it is synchronous. [Module and handles](<https://docs.dhtmlx.com/planning-engine/wasm/module-and-handles.md>) covers [`initSync`](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md#initsync>) and other ways to instantiate.

## Schedule a snapshot

Start with the smallest useful plan: two tasks and a finish-to-start link. Both tasks start on Monday, 2026-08-10, at 08:00, so task 2 starts too early. It must wait for task 1 to finish.

Save this as `schedule.cjs`:

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

const data = {
  tasks: [
    { id: 1, start_date: "2026-08-10 08:00", duration: 2 },
    { id: 2, start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [{ id: 1, source: 1, target: 2, type: "fs" }],
};

const response = JSON.parse(schedule(JSON.stringify(data), "{}"));
if (!response.ok) {
  const { kind, message, path } = response.error;
  console.error(`${kind}: ${message} (at ${path})`);
  process.exit(1);
}
for (const update of response.result.updates) {
  console.log(`task ${update.id}: ${update.start_date} -> ${update.end_date}`);
}
```

Run it with `node schedule.cjs`:

```
task 2: 2026-08-12 08:00 -> 2026-08-13 08:00
```

[`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>) takes a [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/project-data.md>) snapshot and [`SolveOptions`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/solve-options.md>) as JSON text (`"{}"` means all defaults). It returns JSON text too, an envelope that looks like this:

```json
{
  "ok": true,
  "result": {
    "baselineId": "1-35c089b962e633b0",
    "conflicts": [],
    "converged": true,
    "cycles": [],
    "notes": [],
    "updates": [
      { "end_date": "2026-08-13 08:00", "id": 2, "kind": "task", "start_date": "2026-08-12 08:00" }
    ]
  }
}
```

`result` is a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md>). Its `updates` list holds only the tasks that moved. Task 1 didn't move, so it's not in the list. The `baselineId` is an opaque token, so don't compare it across runs.

Durations are working time in days. The default calendar works Monday to Friday, 08:00-12:00 and 13:00-17:00. That's why the two-day task 1 ends on Wednesday at 08:00, not on Tuesday evening.

Dates are strings: local time, minute precision, no time zone. Output dates are always `YYYY-MM-DD HH:mm`. Ids are numbers or strings, and `1` and `"1"` name the same task. Field names follow the [wire format](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md>): task data is snake_case (`start_date`), while options and most result fields are camelCase (`baselineId`).

### When the input is invalid

Every leaf task needs two of `start_date`, `end_date` and `duration`. Remove task 2's `start_date` and keep only its duration. [`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>) doesn't throw. It returns an [error envelope](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md#envelope>):

```json
{
  "error": {
    "code": null,
    "kind": "validation",
    "message": "task needs two of start_date / end_date / duration",
    "path": "tasks[1].start_date"
  },
  "ok": false
}
```

The script prints it and exits with code 1:

```
validation: task needs two of start_date / end_date / duration (at tasks[1].start_date)
```

`path` points to the field that failed. Add the start date back and the script runs again.

Bad input is the only thing that fails this call. Dependency cycles, constraint conflicts and dropped links come back as data in an `ok: true` result (`cycles`, `conflicts`, `notes`).

## Keep a live project

[`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>) fits batch jobs and request handlers that see each snapshot once. An editor works differently. It loads the plan once, then sends small changes as the user works. That's what a [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) handle does.

This script loads the same two tasks, previews a change, then commits it. Save it as `live.cjs`:

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

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

const data = {
  tasks: [
    { id: 1, start_date: "2026-08-10 08:00", duration: 2 },
    { id: 2, start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [{ id: 1, source: 1, target: 2, type: "fs" }],
};

const project = new JsonProject("{}");
try {
  const first = unwrap(project.solve(JSON.stringify(data), "{}"));
  console.log(`loaded, ${first.updates.length} task(s) moved`);

  // The user stretches task 1 to three days.
  const changes = JSON.stringify({ tasks: { update: [{ id: 1, duration: 3 }] } });

  const preview = unwrap(project.preview(changes, JSON.stringify({ baselineId: first.baselineId })));
  console.log(`preview: ${preview.updates.length} task(s) would move`);

  const report = unwrap(project.apply(changes));
  if (report.needsResync) {
    throw new Error("an edit was rejected; reload the project with solve(dataJson, ...)");
  }

  const result = unwrap(project.solve(undefined, "{}"));
  for (const update of result.updates) {
    console.log(`task ${update.id}: ${update.start_date} -> ${update.end_date}`);
  }
} finally {
  project.free();
}
```

Run it with `node live.cjs`:

```
loaded, 1 task(s) moved
preview: 2 task(s) would move
task 1: 2026-08-10 08:00 -> 2026-08-13 08:00
task 2: 2026-08-13 08:00 -> 2026-08-14 08:00
```

Each call does one job:

- [`new JsonProject("{}")`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) creates an empty project with default options.
- [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) with data loads and schedules the snapshot. Its dates become the project's baseline.
- [`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>) runs the change on a copy of the baseline. The project doesn't change, so it's safe to call on every mouse move during a drag.
- [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) commits the change and marks the project as unsolved. It returns an [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#applyreport>).
- [`solve(undefined, ...)`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) reschedules the current state. Task 1 is reported because it was edited, and task 2 moves to follow it.
- [`free()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#free>) releases the project's WebAssembly memory. The `finally` block makes sure it runs even when a call fails.

Edits are [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>). A [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/task-update.md>) changes only the fields you send. A field you leave out stays as it is, and `null` clears it.

The handle lives in WebAssembly memory, not in the JS heap. Call `free()` on every project you create, and on every [`clone()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#clone>). A finalizer frees leaked handles eventually, but only as a fallback. After `free()`, any call on the handle throws.

### When the options are invalid

The [constructor](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#creating>) is the one export that throws on bad input. It throws the error envelope as a **string**, not an `Error`. Pass an unknown validation mode and you get this:

```js
try {
  new JsonProject('{"validation":"loose"}');
} catch (thrown) {
  console.log(JSON.parse(thrown).error.message); // unknown validation mode
}
```

Every other method returns its errors in the envelope, as `unwrap` expects.

### When an edit is rejected

[`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) accepts what it can and reports the rest. A bad operation doesn't produce an error envelope. Remove a task that doesn't exist, and you still get `ok: true`, with the problem in `rejected`:

```json
{
  "needsResync": true,
  "rejected": [{ "message": "unknown task id", "path": "tasks.remove[0]" }]
}
```

`needsResync` is now `true`. Your copy of the plan and the project's copy no longer match, so the next [`solve(undefined, ...)`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) fails:

```json
{
  "error": { "code": "needs-resync", "kind": "project", "message": "needs-resync", "path": null },
  "ok": false
}
```

To fix it, send the full data again with `solve(JSON.stringify(data), ...)`. That's why the example checks `needsResync` before it solves. [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/recover-from-rejected-changes.md>) covers this flow in detail.

## Next steps

- [JavaScript usage](<https://docs.dhtmlx.com/planning-engine/wasm/javascript.md>): the `Project` glue, and how it differs from the raw surface.
- [Wire format](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md>): the envelope, field spelling, ids, dates and numbers.
- [Module and handles](<https://docs.dhtmlx.com/planning-engine/wasm/module-and-handles.md>): build targets, instantiation, freeing handles and threads.
- [Project data](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/project-data.md>): tasks, links, calendars, resources and assignments.
- [Scheduling a project](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/scheduling-a-project.md>): one-shot and stateful scheduling, options and subtrees.
- [Reading results](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/reading-results.md>): updates, conflicts, cycles and notes.
- [Working-time calendars](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/working-time-calendars.md>): hours, weekdays and date exceptions.
- [Editing a project](<https://docs.dhtmlx.com/planning-engine/wasm/live-projects/editing-a-project.md>): `apply`, baselines and resync.
- [Previewing changes](<https://docs.dhtmlx.com/planning-engine/wasm/live-projects/previewing-changes.md>): what-if results without committing.
- [Leveling resources](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/leveling-resources.md>): remove resource overallocation.
- [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>): error envelopes, host-level throws and what's data instead of an error.
- [API reference](<https://docs.dhtmlx.com/planning-engine/wasm/api/overview.md>): every export and payload.
