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. 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:
The token comes from an environment variable, so the file can go into source control. Set the variable in your shell:
# bash / zsh
# 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.
Yarn 2 and later read .yarnrc.yml instead of .npmrc:
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:
Trial edition, installed under the full name with an npm alias:
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() exists, but you don't need to call it: it loads nothing and resolves immediately.
Move from the trial to the full edition#
- In
.npmrc, changeplen-trialtoplenon both lines, and use a token or the username and password of your full-edition account. - Run
npm install @dhx/plen-js@^1. It replaces the alias inpackage.jsonwith 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#
E401orUnable to authenticate: the token or password is wrong, or the variable isn't set in the shell that runsnpm. 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-trialon theplen-trialregistry. Also check that the URL ends in/npm/on both lines.- Requests go to
npm.dhtmlx.comorregistry.npmjs.org: another@dhx:registryline wins. Runnpm config get @dhx:registryin the project folder. A project.npmrcoverrides your user config. - The lockfile still points at the old registry after a change: the lockfile stores full tarball URLs. Delete
node_modulesand 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.
const = ;
Save it as main.mjs and run node main.mjs:
task 2: 2026-08-12 08:00 -> 2026-08-13 08:00
schedule takes a ProjectData object and returns a ScheduleResult. 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 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 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 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 (cycles, conflicts, notes).
Keep a live project#
schedule 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 does.
This program loads the same two tasks, previews a change, then commits it:
const = ;
const = new;
const = ;
;
// The user stretches task 1 to three days.
const = ;
const = ;
;
const = ;
const = ;
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()creates an empty project. It throws anInputErrorif you pass an unknown option or an invalid value.project.solve(data)loads and schedules the data. Its dates become the project's baseline, andbaselineIdnames that baseline.project.preview(changes, { baselineId })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 readbaselineId, it throws aProjectErrorwith the code"stale-baseline".project.apply(changes)commits the change and marks the project as unsolved.project.solve(null)reschedules the current state. Task 1 is reported because it was edited, and task 2 moves to follow it.
Edits are SchedulingChanges objects with add, update and remove lists per tasks, links, resources and assignments. A TaskUpdate 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 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 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 covers this flow in detail.
Where to go next#
- Plain objects and dates: how inputs are read and how to get
Dateobjects back. - Project data: tasks, links, calendars, resources and assignments.
- Scheduling a project: one-shot and stateful scheduling, options and subtrees.
- Reading results: updates, conflicts, cycles and notes.
- Working-time calendars: hours, weekdays and date exceptions.
- Data updates:
apply, baselines and resync. - Previewing changes: what-if results without committing.
- Leveling resources: remove resource overallocation.
- Error handling:
InputError,ProjectErrorand what comes back as data instead of an error. - API reference: every public class, function and type.