[Canonical page](<https://docs.dhtmlx.com/planning-engine/js/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 JavaScript program that schedules two linked tasks, and a live project that reschedules them when one changes.

## Requirements

- Node.js 18 or later, or any modern browser with a bundler.
- No WebAssembly and no runtime dependencies. Every call is synchronous and runs on the calling thread.

## 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-js` |
| Trial | `https://git.dhtmlx.com/api/packages/plen-trial/npm/` | `@dhx/plen-js-trial` |

The trial is the full engine with one limit: a project with more than 100 tasks is rejected with an [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>). The API is the same.

Each account sees only its own edition. Trial credentials can't install `@dhx/plen-js`, 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-js
```

Trial edition, installed under the full name with an npm alias:

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

With pnpm or Yarn, use `pnpm add` or `yarn add` with the same argument. Thanks to the alias, your code imports `"@dhx/plen-js"` with either edition, as the examples on this page do. You can also install `@dhx/plen-js-trial` under its own name and import from `"@dhx/plen-js-trial"`.

The package has no dependencies. It ships ES module and CommonJS builds with bundled type declarations. You can create a project right after the import. The default export [`init()`](<https://docs.dhtmlx.com/planning-engine/js/api/init.md>) exists, but you don't need to call it: it loads nothing and resolves immediately.

### Move from the trial to the full edition

1. In `.npmrc`, change `plen-trial` to `plen` on both lines, and use a token or the username and password of your full-edition account.
2. Run `npm install @dhx/plen-js@^1`. It replaces the alias in `package.json` with the real package, so your code doesn't change.

If you installed `@dhx/plen-js-trial` under its own name, also run `npm uninstall @dhx/plen-js-trial` and change the imports to `"@dhx/plen-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-js-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.

## 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.

```js
import { schedule, InputError } from "@dhx/plen-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" }],
};

try {
  const result = schedule(data);
  for (const update of result.updates) {
    console.log(`task ${update.id}: ${update.start_date} -> ${update.end_date}`);
  }
} catch (error) {
  if (error instanceof InputError) {
    console.error(`invalid input at ${error.path}: ${error.message}`);
  } else {
    throw error;
  }
}
```

Save it as `main.mjs` and run `node main.mjs`:

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

[`schedule`](<https://docs.dhtmlx.com/planning-engine/js/api/schedule.md>) takes a [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/js/api/types/project-data.md>) object and returns a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md>). Its `updates` array holds only the tasks that moved. Task 1 didn't move, so it's not in the list.

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 `"YYYY-MM-DD HH:MM"` strings in local time, with minute precision and no time zone. Input also accepts `Date` objects. `schedule` always returns strings; to get `Date` objects back, use a [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md#projectoptions>) created with `{ dates: "date" }`. Ids can be numbers or strings, and `1` and `"1"` name the same task.

### 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/js/api/schedule.md>) now throws before it schedules anything, and the `catch` block prints:

```
invalid input at tasks[1].start_date: task needs two of start_date / end_date / duration
```

The [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md>) has a `path` that points to the field that failed. Put the start date back and the program runs again.

Bad input is the only thing that makes this call throw. Dependency cycles, constraint conflicts and dropped links come back as data in the [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md>) (`cycles`, `conflicts`, `notes`).

## Keep a live project

[`schedule`](<https://docs.dhtmlx.com/planning-engine/js/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 [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>) does.

This program loads the same two tasks, previews a change, then commits it:

```js
import { Project } from "@dhx/plen-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 project = new Project();
const first = project.solve(data);
console.log(`loaded, ${first.updates.length} task(s) moved`);

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

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

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

const result = project.solve(null);
for (const update of result.updates) {
  console.log(`task ${update.id}: ${update.start_date} -> ${update.end_date}`);
}
```

Output:

```
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 Project()`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>) creates an empty project. It throws an [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) if you pass an unknown option or an invalid value.
- [`project.solve(data)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>) loads and schedules the data. Its dates become the project's baseline, and `baselineId` names that baseline.
- [`project.preview(changes, { baselineId })`](<https://docs.dhtmlx.com/planning-engine/js/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. If the baseline has moved on since you read `baselineId`, it throws a [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>) with the code `"stale-baseline"`.
- [`project.apply(changes)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>) commits the change and marks the project as unsolved.
- [`project.solve(null)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>) reschedules the current state. Task 1 is reported because it was edited, and task 2 moves to follow it.

Edits are [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/js/api/types/scheduling-changes.md>) objects with `add`, `update` and `remove` lists per tasks, links, resources and assignments. A [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/js/api/types/task-update.md>) sets only the fields you give it. A field set to `null` is cleared, and a field you leave out stays as it is.

### When an edit is rejected

[`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>) accepts what it can and lists the rest in the returned report. It doesn't throw for a bad operation. Remove a task that doesn't exist, `{ tasks: { remove: [99] } }`, and the report gets this entry in `rejected`:

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

`needsResync` is now `true`. Your copy of the plan and the project's copy no longer match, so the next `project.solve(null)` throws a [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md>) with the code `"needs-resync"`. To fix it, send the full data again with `project.solve(data)`. That's why the example checks `needsResync` before it solves. [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/js/how-to/recover-from-rejected-changes.md>) covers this flow in detail.

## Where to go next

- [Plain objects and dates](<https://docs.dhtmlx.com/planning-engine/js/plain-objects-and-dates.md>): how inputs are read and how to get `Date` objects back.
- [Project data](<https://docs.dhtmlx.com/planning-engine/js/scheduling/project-data.md>): tasks, links, calendars, resources and assignments.
- [Scheduling a project](<https://docs.dhtmlx.com/planning-engine/js/scheduling/scheduling-a-project.md>): one-shot and stateful scheduling, options and subtrees.
- [Reading results](<https://docs.dhtmlx.com/planning-engine/js/scheduling/reading-results.md>): updates, conflicts, cycles and notes.
- [Working-time calendars](<https://docs.dhtmlx.com/planning-engine/js/scheduling/working-time-calendars.md>): hours, weekdays and date exceptions.
- [Data updates](<https://docs.dhtmlx.com/planning-engine/js/live-projects/editing-a-project.md>): `apply`, baselines and resync.
- [Previewing changes](<https://docs.dhtmlx.com/planning-engine/js/live-projects/previewing-changes.md>): what-if results without committing.
- [Leveling resources](<https://docs.dhtmlx.com/planning-engine/js/resources-and-assignment/leveling-resources.md>): remove resource overallocation.
- [Error handling](<https://docs.dhtmlx.com/planning-engine/js/error-handling.md>): `InputError`, `ProjectError` and what comes back as data instead of an error.
- [API reference](<https://docs.dhtmlx.com/planning-engine/js/api/overview.md>): every public class, function and type.
