[Canonical page](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/recover-from-rejected-changes/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Recover from rejected changes

Get an editor working again after [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) rejects part of a [change set](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>), without reloading the module or losing the user's edits.

A rejection isn't an error envelope. `apply` returns `ok: true` with the refused operations in [`rejected`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#applyreport>), and sets `needsResync`. From then on, [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) without data, `apply` and [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) return a `project` error with code `needs-resync`. Calls that need a baseline ([`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>), [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md>), [`evaluate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md>), [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-suggest-assignments.md>)) return `no-baseline`. To clear this state, send the full data again with `solve(dataJson, …)`. See [Editing a project](<https://docs.dhtmlx.com/planning-engine/wasm/live-projects/editing-a-project.md#rejections-and-resync>) for why.

## Prerequisites

- A long-lived [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>), solved once with [`solve(dataJson, …)`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>).
- Your own model that can produce the full [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/project-data.md>) the user expects to see. You resync from this data.

## Steps

1. Apply the change set and keep the [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#applyreport>) from the envelope's `result`:

   ```js
   const report = unwrap(project.apply(JSON.stringify(changes)));
   ```

   An [error envelope](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md#envelope>) here means nothing was applied: `needs-resync` from an earlier rejection, `stale-baseline`, or a `parse` / `validation` error in the change set. A rejection inside the batch isn't an error envelope.

   A `priority` outside 0..1000 or a malformed hour range fails the whole call with a `validation` error that has no `path`. In `apply`, this means no operation from that change set is applied.

2. Check `needsResync`. If it's `false`, continue as usual with [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) without data:

   ```js
   if (!report.needsResync) {
     unwrap(project.solve(undefined, "{}"));
   }
   ```

3. Show the user what was refused. Each entry of `rejected` has the operation's `path` (such as `tasks.remove[0]`) and a `message`:

   ```js
   for (const op of report.rejected) {
     console.error(`rejected ${op.path}: ${op.message}`);
   }
   ```

   The other operations in the batch were applied. A rejected operation leaves no partial edit behind.

4. Make your model hold the state you want. Undo the rejected edits in it, or keep it as is if it's already correct. Don't try to repair the project with more [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) calls: they return `needs-resync`.

5. Resync with the full data. Set `includeUnchanged` in the [solve options](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/solve-options.md>) so the result carries the dates of every task, not only the ones that moved:

   ```js
   const resync = JSON.stringify({ includeUnchanged: true });
   const result = unwrap(project.solve(JSON.stringify(model), resync));
   ```

6. Write every entry of [`result.updates`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#updates>) back into your UI. The engine's state may differ from what the UI shows, so refresh all task dates, not just the edited ones.

7. Take the new `baselineId` from `result` for the next [`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>) or change set. Tokens from before the resync are stale.

The full example below stretches one task and, in the same batch, removes a task the engine doesn't have:

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

// The editor's own model: the data you resync from.
const model = {
  // 24/7 time keeps the example dates easy to check.
  config: { use_calendars: false },
  tasks: [{ id: "design", start_date: "2026-01-05 00:00", duration: 2 }],
};

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

  // The user stretches "design" to 4 days. The UI also sends a removal
  // of "review", a task the engine never had.
  model.tasks[0].duration = 4;
  const changes = {
    tasks: { update: [{ id: "design", duration: 4 }], remove: ["review"] },
  };

  const report = unwrap(project.apply(JSON.stringify(changes)));
  if (!report.needsResync) {
    unwrap(project.solve(undefined, "{}"));
  } else {
    for (const op of report.rejected) {
      console.error(`rejected ${op.path}: ${op.message}`);
    }

    const resync = JSON.stringify({ includeUnchanged: true });
    const response = JSON.parse(project.solve(JSON.stringify(model), resync));
    if (response.ok) {
      for (const u of response.result.updates) {
        console.log(`${u.id} ${u.start_date}..${u.end_date}`);
      }
    } else {
      // The project keeps its state and still needs a resync.
      const { kind, message, path } = response.error;
      console.error(`resync data refused (${kind}) at ${path}: ${message}`);
    }
  }
} finally {
  project.free();
}
```

The report from that `apply` looks like this:

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

The real report also carries [`effects`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#changeeffects>), omitted here.

## When the resync data is invalid

The resync goes through the same validation as the first load. If your model produces invalid data, such as a duplicate task id, [`solve(dataJson, …)`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) returns a `validation` error envelope with the field path:

```json
{
  "ok": false,
  "error": { "kind": "validation", "code": null, "message": "duplicate task id \"design\"", "path": "tasks[1].id" }
}
```

The project keeps its previous state and still returns `needs-resync`. Fix the data and call `solve(dataJson, …)` again. See [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>) for the envelope and the error kinds.

## Drop the edit instead of resyncing

To avoid a full reload, apply the change set to a [`clone()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#clone>) first. A clone is cheap, and a rejection there doesn't touch the original:

```js
const draft = project.clone();
const report = unwrap(draft.apply(JSON.stringify(changes)));
if (report.needsResync) {
  // `project` still has its baseline; roll the edit back in your model too.
  draft.free();
} else {
  unwrap(draft.solve(undefined, "{}"));
  project.free();
  project = draft;
}
```

Use this when the user should get either the whole change set or none of it. Free whichever handle you drop: each one holds its own WebAssembly memory. See [Module and handles](<https://docs.dhtmlx.com/planning-engine/wasm/module-and-handles.md>).

## Result

Run the example with `node`. It prints the rejected removal on stderr and the refreshed date of every task on stdout:

```text
rejected tasks.remove[0]: unknown task id
design 2026-01-05 00:00..2026-01-09 00:00
```

After the resync, [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>), [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) without data, [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) and [`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>) work again, and `needsResync` is `false` for the next valid change set.
