[Canonical page](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# JsonProject.apply

Writes the edits of a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) payload into the accepted state of a [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) and marks it unsolved. It doesn't schedule anything: follow it with [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) without data or with [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) to get the new dates. To see what an edit would do without committing it, use [`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>).

## Signature

```ts
apply(changes_json: string): string
```

Synchronous and CPU-bound. Edits the handle in place. In the browser build, call it only after [`init()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md>) has resolved.

## Request

| Argument | Payload | Required | Description |
|---|---|---|---|
| `changes_json` | [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) | yes | The edits to commit. The order the operations run in is on the [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) page. |

## Behavior

- Operations are partially accepted. Each one is tried on its own, so a rejected operation leaves no partial edit and the operations around it still apply. Rejections go into `rejected` of a success envelope, not into an error envelope.
- Any rejection sets `needsResync`. After that, `solve` without data, `apply` and `level` return `needs-resync` until a successful `solve` with full data.
- The project is unsolved afterwards. `preview`, `loadProfile`, `evaluate`, `slots` and `suggestAssignments` return `no-baseline` until the next solve, and every earlier `baselineId` is stale.
- Tasks added or updated by successful operations are reported by the next acyclic `solve` without data, or `level`, even when the planner leaves them where the edit put them. They accumulate across several `apply` calls until then.
- A [`clone()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#clone>) made before the call doesn't see the edit.

## Response

The success envelope carries an [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#applyreport>). The envelope shape is described in [Wire format](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md#envelope>).

### ApplyReport

Which operations of the change set took effect, which were refused, and what the accepted ones changed beyond the records they named. All fields are always present.

| Field | Type | Description |
|---|---|---|
| `applied` | `{ path: string }[]` | Accepted operations, in the order they ran. `path` names the operation, e.g. `tasks.update[0]`. |
| `rejected` | `{ path: string, message: string }[]` | Refused operations, in the order they ran, each with the reason. Empty when the whole change set was accepted. |
| `effects` | [`ChangeEffects`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#changeeffects>) | Ids removed, generated or dropped by the accepted operations. |
| `needsResync` | `boolean` | `true` when `rejected` isn't empty. |
| `revision` | `number` | The project's state revision after this call. Every `apply` raises it, so a larger value from the same handle means a later state. |

Every operation of the change set appears in exactly one of `applied` and `rejected`.

### ChangeEffects

Side effects of the accepted operations, for keeping the caller's own copy of the data in sync. Every field is an array of [`Id`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#id>)s, always present and empty when there was no such effect. A rejected operation adds nothing.

| Field | Type | Description |
|---|---|---|
| `removedTasks` | `Id[]` | Each task named in `tasks.remove`, followed by its descendants, which are removed with it. |
| `removedLinks` | `Id[]` | Links named in `links.remove`, and links removed because their source or target task was removed. |
| `generatedLinks` | `Id[]` | Ids given to added links that had no `id`, in the form `$generated_link_1`, `$generated_link_2`, and so on. |
| `droppedLinks` | `Id[]` | Links still in the project that became inactive because of this change set. Links that were already inactive aren't listed. |
| `removedAssignments` | `Id[]` | Assignments named in `assignments.remove`, and assignments removed because their task or resource was removed. |
| `generatedAssignments` | `Id[]` | Ids given to added assignments that had no `id`. |
| `droppedAssignments` | `Id[]` | Assignments still in the project that became inactive because of this change set. |

## Errors

| Error | When |
|---|---|
| `kind: "project"`, `code: "stale-baseline"` | `baselineId` in the change set doesn't name the current baseline. Nothing is applied. |
| `kind: "project"`, `code: "needs-resync"` | An earlier `apply` rejected an operation and no `solve` with data followed. Checked before `baselineId`, so it wins over `stale-baseline`. |
| `kind: "parse"` | `changes_json` isn't valid JSON, has the wrong JSON type, misses a required field, or has an unknown field in the change set or its buckets. |
| `kind: "validation"` | A value the decoder rejects, such as an invalid date or an unknown enum string (`path: null`). Nothing is applied. |

Errors come back in the returned envelope; the call doesn't throw for bad input. Calling it on a freed handle throws at host level. See [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>).

:::note
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.
:::

:::note
A start date that falls on non-working time is kept unsnapped when the task has no predecessor. The end is computed forward from it on the working calendar.
:::

## Example

Request (`changes_json`), sent after a `solve` that returned `baselineId` `"1-b0605af8cba3d7c9"`:

```json
{
  "baselineId": "1-b0605af8cba3d7c9",
  "tasks": {
    "update": [{ "id": "design", "duration": 3 }],
    "remove": ["gone"]
  },
  "links": {
    "add": [{ "source": "design", "target": "build", "type": "ss" }]
  }
}
```

Response:

```json
{
  "ok": true,
  "result": {
    "applied": [{ "path": "tasks.update[0]" }, { "path": "links.add[0]" }],
    "effects": {
      "droppedAssignments": [],
      "droppedLinks": [],
      "generatedAssignments": [],
      "generatedLinks": ["$generated_link_1"],
      "removedAssignments": [],
      "removedLinks": [],
      "removedTasks": []
    },
    "needsResync": true,
    "rejected": [{ "message": "unknown task id", "path": "tasks.remove[0]" }],
    "revision": 2
  }
}
```

The project has no task `gone`, so the removal is rejected and `needsResync` is set. The update and the link addition still apply.

Host call in Node, resyncing with full data after a rejection:

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

const data = {
  tasks: [
    { id: "design", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "build", start_date: "2026-08-10 08:00", duration: 3 },
  ],
  links: [{ id: "l1", source: "design", target: "build", type: "fs" }],
};

const project = new JsonProject("{}");
try {
  const solved = JSON.parse(project.solve(JSON.stringify(data), "{}"));
  if (!solved.ok) throw new Error(solved.error.message);

  const changes = {
    baselineId: solved.result.baselineId,
    tasks: { update: [{ id: "design", duration: 3 }], remove: ["gone"] },
  };
  const envelope = JSON.parse(project.apply(JSON.stringify(changes)));
  if (!envelope.ok) {
    const { kind, code, message } = envelope.error;
    throw new Error(`${kind} ${code ?? ""}: ${message}`);
  }

  const report = envelope.result;
  for (const op of report.rejected) {
    console.log(`rejected ${op.path}: ${op.message}`);
  }

  // After a rejection, only solve with full data is accepted.
  const next = report.needsResync
    ? project.solve(JSON.stringify(data), "{}")
    : project.solve(undefined, "{}");
  console.log(JSON.parse(next).result.updates);
} finally {
  project.free();
}
```

## Related

- [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) - the change set
- [`JsonProject.solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) - reschedule after the edits
- [`JsonProject.preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>) - try the edits without committing them
- [Editing a Project](<https://docs.dhtmlx.com/planning-engine/wasm/live-projects/editing-a-project.md>) - change sets, applying and rescheduling
- [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/recover-from-rejected-changes.md>) - handling `rejected` operations and `needs-resync`
