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


# Previewing Changes

This guide shows how to see what an edit would do before you commit it through the raw WebAssembly exports. It covers previewing a drag or a resize, keeping previews in sync with the project's baseline, checking the resource load a change would cause, and trying multi-step edits or a leveling run on a copy of the project.

## How previews work

A solved [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) holds one **accepted baseline**: the dates from its last [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) or [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>). Every preview starts from that baseline.

[`preview(changes_json, options_json)`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>) copies the baseline, applies a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) payload to the copy and schedules it. The copy is dropped when the call returns. The project stays as it was, so you can call `preview` as often as the UI needs, for example on every mouse move during a drag.

The envelope carries a regular [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md>):

- [`updates`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#mutation>) are relative to the baseline. They list the tasks the change would move and the tasks the change itself touched, like the `solve(undefined, …)` after an [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>). An empty change set (`"{}"`) gives empty `updates`.
- [`conflicts`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#conflict>), [`cycles`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#cycle>) and [`notes`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#note>) describe the previewed state. A preview can show the cycle a new link would create, and the project never holds that cycle.
- [`baselineId`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#baselineid>) names the baseline the preview read.

A preview differs from `apply` in two ways. It isn't partial. If any operation in the change set is invalid, the whole call returns a `validation` error envelope for the first rejected operation. It also needs a clean, solved baseline. Before the first solve, and after any `apply` until the next `solve`, it returns a `project` error with code [`no-baseline`](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md#project-errors>).

For a change that takes more than one step, or for a `level` run, use [`clone()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#clone>) instead. The clone is a new handle that shares immutable state with the original until one of them is edited, so it's cheap to create. Edit, solve or level the clone, and the original stays as it was.

| You want to see | Use |
|---|---|
| The effect of one change set (a drag, a resize, a new link) | `preview` |
| The load a change would put on resources | `preview` with `profile` in the options |
| Several edits in a row, each solved | `clone()`, then `apply` + `solve(undefined, …)` on the copy |
| What leveling would do | `clone()`, then `level` on the copy |

## Preview

Build the edit as a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) payload, the same JSON you'd pass to [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>), and pass it to [`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>).

```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 = {
  // All time is working time: plain day arithmetic, no weekends.
  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", type: "fs" }],
};

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

  // The user drags task "b" to January 5.
  const drag = { tasks: { update: [{ id: "b", start_date: "2026-01-05" }] } };
  const preview = unwrap(project.preview(JSON.stringify(drag), "{}"));
  for (const update of preview.updates) {
    console.log(`task ${update.id}: ${update.start_date} .. ${update.end_date}`);
  }

  // The project still holds the solved dates: "b" starts on January 3.
  const current = unwrap(project.solve(undefined, "{}"));
  console.log(current.updates.every((u) => u.start_date !== "2026-01-05 00:00")); // true
} finally {
  project.free();
}
```

The solve puts `b` right after `a`, on January 3. The preview prints `task b: 2026-01-05 00:00 .. 2026-01-06 00:00`. The project doesn't change: the next `solve(undefined, "{}")` returns the cached result of the first solve.

When the user drops the task, send the same change set to `apply` and then call `solve(undefined, …)`. See [Editing a project](<https://docs.dhtmlx.com/planning-engine/wasm/live-projects/editing-a-project.md>).

The [preview options](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md#previewoptions>) take the same diagnostic switches as [`SolveOptions`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/solve-options.md>):

- [`subtree`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#subtree>) previews only one branch.
- `reasons: true` adds a [`reason`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#reason>) to each update, which tells you why a task would move.
- [`analytics`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#analyticsoptions>) adds the critical path and slack of the previewed state.

Previews stay sparse: there's no `includeUnchanged` option. The full option list is on the [`preview` reference](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md#previewoptions>).

## Guarding with baseline tokens

A UI that previews in the background can run into a race. Another request applies an edit or reloads the data, and the preview now runs against a baseline the user never saw. Baseline tokens catch this.

Every `solve` and `level` result carries [`baselineId`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#baselineid>), an opaque token for the accepted baseline. Send it back as `baselineId` in the [preview options](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md#previewoptions>) (or in the [change set](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>)). If the token no longer names the current baseline, `preview` returns a `project` error with code [`stale-baseline`](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md#project-errors>) instead of dates for the wrong state.

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

const data = {
  config: { use_calendars: false },
  tasks: [{ id: "a", start_date: "2026-01-01", duration: 2 }],
};
const resize = JSON.stringify({ tasks: { update: [{ id: "a", duration: 3 }] } });

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

  // The UI keeps the token of the dates it shows.
  const shown = loaded.baselineId;
  const guarded = JSON.stringify({ baselineId: shown });

  const preview = JSON.parse(project.preview(resize, guarded));
  // A preview reports the baseline it read, so its token works for the next preview too.
  console.log(preview.result.baselineId === shown); // true

  // Meanwhile, another edit is committed.
  project.apply(resize);

  // Until the next solve there is no baseline to preview against.
  const early = JSON.parse(project.preview(resize, "{}"));
  console.log(early.error.code); // "no-baseline"

  const latest = JSON.parse(project.solve(undefined, "{}")).result;

  // The old token no longer names the baseline.
  const stale = JSON.parse(project.preview(resize, guarded));
  if (!stale.ok && stale.error.code === "stale-baseline") {
    // Refresh the view from `latest`, then preview with its token.
    const fresh = JSON.stringify({ baselineId: latest.baselineId });
    const retry = JSON.parse(project.preview(resize, fresh));
    console.log(retry.ok); // true
  }
} finally {
  project.free();
}
```

The stale call returns this envelope:

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

A few rules for tokens:

- Take the token from the latest [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) or [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) result. A preview result carries the token it read, so a chain of previews during one drag can reuse it.
- The token changes whenever the data changes: new data, an [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>), a solve that moves dates, leveling edits.
- Tokens are opaque. Don't parse them, store them across runs or compare them between runs. The same data can get a new token on a repeat run.
- `preview` checks `baselineId` in the options and `baselineId` in the change set separately. Leave both out to skip the check.

For how to handle `stale-baseline` and the other project errors, see [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>).

## Preview with load

Add a [`profile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#profilequery>) query to the preview options to see the resource load a change would cause. The result then has `profile`, the same per-bucket load and capacity that [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md>) returns, but computed on the previewed dates. Use it to show a resource histogram while the user drags a task.

```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 = {
  config: { use_calendars: false },
  tasks: [
    { id: "a", start_date: "2026-01-01", duration: 2 },
    { id: "b", start_date: "2026-01-03", duration: 1 },
  ],
  resources: [{ id: "dev", capacity: 1 }],
  assignments: [
    { task: "a", resource: "dev", value: 1 },
    { task: "b", resource: "dev", value: 1 },
  ],
};

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

  // Drag "b" one day earlier, onto the last day of "a".
  const drag = { tasks: { update: [{ id: "b", start_date: "2026-01-02" }] } };
  const options = {
    profile: { from: "2026-01-01", to: "2026-01-05", tasks: "overallocated" },
  };
  const preview = unwrap(project.preview(JSON.stringify(drag), JSON.stringify(options)));

  for (const resource of preview.profile.resources) {
    for (const bucket of resource.buckets.filter((b) => b.load > b.capacity)) {
      console.log(
        `${resource.resource} overloaded at ${bucket.start}: ` +
          `${bucket.load} of ${bucket.capacity} (${bucket.tasks.join(", ")})`,
      );
    }
  }
} finally {
  project.free();
}
```

Both tasks need `dev` on January 2, so that bucket's load goes over its capacity of 1, and the code prints `dev overloaded at 2026-01-02 00:00: 2 of 1 (a, b)`. With `tasks: "overallocated"`, each overloaded bucket also lists the tasks that share it.

The query covers buckets that start in `[from, to)`. By default it reports every resource, with the bucket size ([`basis`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#load-basis>)) derived from `config.duration_unit`. Narrow it with `resources`, or switch `basis`. The `profile` object has no `baselineId` of its own. A `baselineId` inside it is an unknown field and fails with a `parse` error, so put the token at the top level of the preview options. Every preview also reports [`overallocations`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/overallocation.md>) when the project has assignments, so you can skip the profile if you need only the overloaded windows. See [Resource load](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/resource-load.md>) and the [`LoadProfile` reference](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/load-profile.md>).

## Cloning a project

[`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>) takes one change set. To see the result of several edits in a row, each one solved before the next, [clone](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#clone>) the project and work on the copy.

```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;
}

function resize(id, duration) {
  return JSON.stringify({ tasks: { update: [{ id, duration }] } });
}

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", type: "fs" }],
};

let project = new JsonProject("{}");
unwrap(project.solve(JSON.stringify(data), "{}"));

// A cheap copy: it shares state with `project` until it's edited.
const draft = project.clone();
unwrap(draft.apply(resize("a", 4)));
unwrap(draft.solve(undefined, "{}"));
unwrap(draft.apply(resize("b", 3)));
const outcome = unwrap(draft.solve(undefined, "{}"));

for (const update of outcome.updates) {
  console.log(`draft task ${update.id}: ${update.start_date} .. ${update.end_date}`);
}

// The original is untouched and still previews against its own baseline.
console.log(unwrap(project.preview("{}", "{}")).updates.length); // 0

// The user accepts the draft: keep it as the project, free the old handle.
project.free();
project = draft;
unwrap(project.solve(undefined, "{}"));
project.free();
```

The last draft solve prints `draft task b: 2026-01-05 00:00 .. 2026-01-08 00:00`. Edits on one copy never affect the other. The copy is a full [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) with its own baseline, cache and `baselineId`, so you can preview, `level`, `evaluate` or keep editing it.

To accept the draft, replace the original with it, or replay the same change sets on the original with `apply`. To discard it, call [`free()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#free>) on it. A clone is a separate handle in WebAssembly memory, so free each one on its own. See [Module and handles](<https://docs.dhtmlx.com/planning-engine/wasm/module-and-handles.md>).

Clone before [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) too. `level` changes the project and accepts the leveled dates as the new baseline, so running it on a copy is how you show the user a leveling proposal before committing it:

```js
function levelingProposal(project) {
  const draft = project.clone();
  try {
    const response = JSON.parse(draft.level("{}"));
    if (!response.ok) throw new Error(response.error.message);
    for (const update of response.result.updates) {
      console.log(`task ${update.id} delay ${update.leveling_delay}`);
    }
  } finally {
    draft.free();
  }
}
```

The step-by-step version is in [Preview leveling](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/preview-leveling.md>). To compare several alternative change sets with scores, use [`evaluate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md>) instead. It works on disposable copies internally. See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/evaluating-alternatives.md>).

Every export is synchronous and blocks the thread that calls it. A module instance is single-threaded, so to keep previews off the UI thread, run the project in a Web Worker or a worker thread. See [Run in a worker](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/run-in-a-worker.md>).
