[Canonical page](<https://docs.dhtmlx.com/planning-engine/js/how-to/run-in-a-worker/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Run in a worker

Keep your UI responsive while a large project is leveled or evaluated by moving the [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>) to a worker thread.

Every `Project` method is synchronous and runs on the thread that calls it. A long [`level`](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md>) or [`evaluate`](<https://docs.dhtmlx.com/planning-engine/js/api/project-evaluate.md>) call on the main thread blocks rendering and input until it returns. Inside a worker, the same call blocks only the worker.

## Prerequisites

- A bundler or runtime that loads module workers: a modern browser, or Node 18 or later.
- Data that's already plain objects. Inputs and results cross the thread boundary with `postMessage`, so they must be structured-cloneable. [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/js/api/types/project-data.md>), change sets and all results are plain objects, arrays, strings, numbers and `Date`s, so they qualify.

## Steps

1. Create the worker file. It owns one [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>), calls the requested method, and posts back either the result or the error fields:

   ```js
   // scheduler.worker.js
   import { Project } from "plen-js";

   const project = new Project();

   self.onmessage = (event) => {
     const { id, method, args } = event.data;
     try {
       const result = project[method](...args);
       self.postMessage({ id, result });
     } catch (e) {
       self.postMessage({
         id,
         error: { name: e.name, message: e.message, kind: e.kind, path: e.path, code: e.code },
       });
     }
   };
   ```

   Send the error as plain fields. A thrown [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) or [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>) doesn't keep its class, `kind`, `path` or `code` after `postMessage`.

   You don't need to call [`init()`](<https://docs.dhtmlx.com/planning-engine/js/api/init.md>); it loads nothing and resolves immediately.

2. On the main thread, start the worker and wrap each message in a promise:

   ```js
   const worker = new Worker(new URL("./scheduler.worker.js", import.meta.url), { type: "module" });

   let nextId = 0;
   const pending = new Map();

   worker.onmessage = (event) => {
     const { id, result, error } = event.data;
     const { resolve, reject } = pending.get(id);
     pending.delete(id);
     if (error) reject(Object.assign(new Error(error.message), error));
     else resolve(result);
   };

   function call(method, ...args) {
     const id = nextId++;
     return new Promise((resolve, reject) => {
       pending.set(id, { resolve, reject });
       worker.postMessage({ id, method, args });
     });
   }
   ```

3. Call the project methods through `call` and `await` the results:

   ```js
   async function main() {
     const data = {
       tasks: [
         { id: "a", start_date: "2026-08-10 08:00", duration: 5, priority: 100 },
         { id: "b", start_date: "2026-08-12 08:00", duration: 2, priority: 900 },
       ],
       resources: [{ id: "dev" }],
       assignments: [
         { task: "a", resource: "dev", value: 1 },
         { task: "b", resource: "dev", value: 1 },
       ],
     };

     await call("solve", data, { includeUnchanged: true });
     const leveled = await call("level", { order: "priority" });
     console.log(leveled.rounds, JSON.stringify(leveled.updates));
   }

   main().catch((e) => console.error(e.name, e.code ?? e.path, e.message));
   ```

4. Check errors by `name` instead of `instanceof`. A rejected call carries the fields the worker sent:

   ```js
   try {
     await call("preview", changes, { baselineId });
   } catch (e) {
     if (e.name === "ProjectError" && e.code === "stale-baseline") {
       // The plan changed since this preview started; drop the stale result.
     }
   }
   ```

Keep the calls for one project in a single worker. The worker handles messages one at a time, so the calls run in the order you send them, as they would on the main thread.

`evaluate` scores candidates one after another, and one worker runs one evaluation at a time.

## Node: use `worker_threads`

In Node, the worker talks to `parentPort` instead of `self`. The method calls and messages stay the same:

```js
// scheduler.worker.js
import { parentPort } from "node:worker_threads";
import { Project } from "plen-js";

const project = new Project();

parentPort.on("message", ({ id, method, args }) => {
  try {
    parentPort.postMessage({ id, result: project[method](...args) });
  } catch (e) {
    parentPort.postMessage({
      id,
      error: { name: e.name, message: e.message, kind: e.kind, path: e.path, code: e.code },
    });
  }
});
```

```js
// main.js
import { Worker } from "node:worker_threads";

const worker = new Worker(new URL("./scheduler.worker.js", import.meta.url));
worker.on("message", ({ id, result, error }) => {
  // Same handler as in step 2.
});
```

## Result

`level` runs in the worker, and the main thread stays free to render and handle input. `leveled.updates` arrives as the same plain objects a direct call returns:

```text
2 [{"end_date":"2026-08-19 08:00","id":"a","kind":"task","pauses":[{"at":2,"by":"leveling","length":2}]}]
```

If you create the project with `new Project({ dates: "date" })`, result dates arrive as `Date` objects too, because `postMessage` copies `Date`s.
