[Canonical page](<https://docs.dhtmlx.com/planning-engine/dhx-gantt-parity/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Using Planning Engine with DHX Gantt

Planning Engine can run as the scheduling and resource-planning engine behind DHTMLX Gantt. Gantt gives you the editor, grid, timeline and resource views. The engine calculates dates, checks the plan and suggests improvements. You can run the engine in the browser through WebAssembly or behind a server API.

There are two main ways to use it:

- **Scheduling compatibility:** move dependency scheduling out of Gantt and keep the behavior of its v2 scheduling engine for supported inputs.
- **Advanced planning:** add resource leveling, what-if previews, comparison of alternatives and assignment suggestions, then show the accepted results in Gantt.

This page targets a Gantt version with native support for task `leveling_delay` and `pauses`. With that support, Gantt renders leveled tasks straight from Planning Engine's updates, including interruptions in work. You don't need synthetic subtasks or a custom split-bar renderer. Rendering and scheduling parity are separate things, though. Gantt can display a leveled result, but its own scheduler doesn't do the engine's resource leveling.

## Scheduling compatibility

### What matches

Planning Engine's planner is a port of the DHTMLX Gantt **v2 auto-scheduling engine**. Its parity suite compares normalized final task dates and scheduling diagnostics against a real Gantt instance.

| Scheduling behavior | Compatibility |
| --- | --- |
| Dependencies | Finish-to-start, start-to-start, finish-to-finish and start-to-finish; positive and negative lag |
| Constraints | ASAP, ALAP and the six dated start/finish constraints |
| Direction | Forward and backward scheduling, including task-specific ASAP/ALAP behavior |
| Preferred dates and gaps | `gap_behavior: "preserve"` and `"compress"` |
| Hierarchy | Summary rollup, summary dependencies, block movement and leaf expansion |
| Working time | Supported task calendars, inherited calendars and explicit dated exceptions |
| Scheduling scope | Whole-project runs and subtree runs that account for connected tasks outside the subtree |
| Fixed work | `auto_scheduling: false`; completed-task protection with `use_progress` |
| Diagnostics | Dependency cycles and constraint conflicts |


### Prepare Gantt data for Planning Engine

Most task field names carry over as is: `start_date`, `end_date`, `duration`, `parent`, `constraint_type`, `constraint_date` and `auto_scheduling`. You still need an adapter for configuration, calendars and assignments. Passing `gantt.serialize()` unchanged won't give you the full integration.

| Gantt data or setting | Planning Engine mapping |
| --- | --- |
| Task collection | `tasks` in the project document |
| `type: "project"` | Accepted as a summary alias; supply both dates or neither, and omit summary `duration` |
| Link types `"0"`, `"1"`, `"2"`, `"3"` | Accepted as FS, SS, FF, SF respectively |
| Link `lag` | Copy it; leave `lag_unit` unset to use the configured duration unit |
| `config.work_time` | Set `config.use_calendars` explicitly |
| Task-calendar scheduling | Set `config.calendar_mode: "task"` |
| `config.inherit_calendar: true` | Use `calendar_mode: "task-inherited"` |
| Effective task calendar | Supply its definition in `calendars` and its ID as task `calendar_id` |
| Virtual root | Copy `config.root_id` if it differs from `0` |
| Auto-scheduling policy | Explicitly map `apply_constraints`, `gap_behavior`, `move_projects`, `project_constraint`, `use_progress`, direction and project bounds into Planning Engine's `config` |
| `gantt.autoSchedule(id, inclusive)` | `solve(null, {subtree: {task: id, inclusive}})` after synchronizing edits |

**Calendar mode is the most important compatibility choice.** Planning Engine defaults to `calendar_mode: "resource"`, where each assignment works on its own resource calendar. Gantt's scheduling model resolves one effective calendar per task. To get matching dates, use `"task"` or `"task-inherited"`, and export the calendar Gantt has already resolved, including any dynamically merged resource calendar. Deduplicate merged calendars by ID. Don't try to recreate Gantt's calendar precedence and merging rules inside the engine.

Planning Engine also defaults to `use_calendars: true`, so leaving it out won't preserve a Gantt setup that has working-time calculation turned off. Keep duration units and calendar definitions in sync between the chart and the engine before you load tasks.

Assignments need their own mapping from Gantt's [resource assignment model](https://docs.dhtmlx.com/gantt/guides/resource-management/):

- Map assignment `task_id` / `resource_id` to `task` / `resource` (the wire format also accepts the original names).
- For `default`, map `delay` to working-time `offset` and leave the duration open to the task end. For `fixedDuration`, copy `duration` too.
- For `fixedDates`, calculate the working-time `offset` and `duration` that those dates cover. Planning Engine stores a task-relative slice. If the dates must stay fixed after later edits, keep the original fixed-date intent in your application.
- Don't forward assignment `mode` as an engine setting. Planning Engine has no assignment mode and clips all slices at the task end, while Gantt's `fixedDuration` can extend past it.
- Normalize assignment `value` and resource `capacity` to the same rate scale. Planning Engine uses `1` for 100% allocation, so a UI value in hours per day needs an explicit conversion.

The [Rust import guide](<https://docs.dhtmlx.com/planning-engine/rust/how-to/import-from-dhtmlxgantt.md>) has a complete typed import example.

### Differences that affect adoption

There are three intentional differences from the tested Gantt reference:

| Case | Planning Engine behavior | Reference Gantt behavior |
| --- | --- | --- |
| Ancestor constraints with `project_constraint: true` | Uses the inherited constraint in planning | Inheritance does not reach date planning |
| Summary-to-descendant dependency with `descendant_links: true` in block mode | Schedules the declared fixture | Throws a scheduling-graph cycle error |
| Completed child inside a moving summary, with `use_progress` | Keeps the completed child fixed | Translates the completed child with the summary |

A few other boundaries matter when you import an existing application:

- **Validation:** strict mode rejects duplicate IDs, incomplete or inconsistent date/duration data, and dated constraints without dates. A leaf usually needs two of start, end and duration. If you supply all three, they must agree. `validation: "repair"` handles supported coercions and returns notes, but a fully undated project still needs an explicit anchor.
- **Units and calendars:** `minute`, `hour` and `day` are supported. `week`, `month`, `year` and calendar `customWeeks` are rejected, including `customWeeks` inside a merged calendar.
- **Time zones:** Planning Engine calculates naive local time at minute precision, with no DST arithmetic. Gantt uses JavaScript dates. The parity harness runs in UTC, so local DST transitions need an explicit policy in your application.
- **UI behavior:** drag snapping, event cancellation, automatic task-type changes, grouping, persistence and undo stay with your application and Gantt. Forward the dates and durations that Gantt core changes; Planning Engine doesn't recreate effort-driven duration rules. Translate scheduling exclusions into `auto_scheduling: false`.
- **Output:** Planning Engine returns an envelope with `updates`, `cycles`, `conflicts`, `notes` and `converged`. Updates include summaries, so apply those too. Show diagnostics instead of treating an empty update list as success.
- **Known edge cases:** in block mode, chained summary-to-summary dependencies mixed with cross-group leaf links can overshoot, and on large chains they can overflow. Some summary/scoped fixtures drift in both engines when you reimport their output as fresh input. Universal idempotency isn't part of the parity claim.

### Keep one scheduling authority

When Planning Engine owns the dates, turn off Gantt's automatic dependency scheduling and route editing actions through your adapter. Gantt's current [auto-scheduling configuration](https://docs.dhtmlx.com/gantt/guides/auto-scheduling/) supports `gantt.config.auto_scheduling = {enabled: false}`. Also make sure your event handlers don't start another scheduling pass while you write engine updates back.

A normal edit cycle looks like this:

1. Import the complete planning document with `solve(data, {includeUnchanged: true})`. You get normalized dates for the first display. Keep the source `leveling_delay` and `pauses`, though: a plain solve doesn't emit those fields when they're unchanged.
2. Turn a user edit into `apply(changes)`, check its report, then call `solve(null)` or `level(options)`.
3. Apply the result fields to your stored document and to Gantt in one batch. Refresh diagnostics and resource views from the same result and baseline.
4. Use the latest `baselineId` when you accept a preview or proposal. If `apply` reports `needsResync`, reconcile the source data and replace the engine state with `solve(data)` before you continue. Some peer operations may have already been applied.

```mermaid
sequenceDiagram
  actor U as User
  participant G as Gantt
  participant A as Your adapter
  participant W as Planning Engine
  A->>W: solve(data, includeUnchanged)
  W-->>A: normalized dates
  A->>G: first display
  U->>G: edit a task
  G->>A: edit event (Gantt auto-scheduling is off)
  A->>W: apply(changes)
  W-->>A: apply report
  A->>W: solve(null) or level(options)
  W-->>A: updates and diagnostics
  A->>G: batchUpdate with the updates
  Note over A,G: Edit handlers ignore events while engine updates are written
```

You can't edit calendar definitions through change sets. A calendar change, or an assignment change that introduces a new merged task calendar, needs a full replacement import. Planning Engine has no public raw-project exporter, so your application has to keep its own authoritative document.

## Advanced planning with Gantt

### Resource leveling with native delay and pause rendering

Ordinary dependency scheduling can leave two tasks competing for the same person. `level()` fixes that by delaying or pausing tasks, then pushing their new dates through dependencies. The leveled project becomes the new baseline.

| Returned task field | Meaning in the chart |
| --- | --- |
| `start_date`, `end_date` | Final task-bar bounds, including the effects of leveling |
| `duration` | Working duration, excluding pauses and non-working time |
| `leveling_delay` | Elapsed displacement; the returned start already includes it |
| `pauses` | Interruptions within the task: `{at, length, by: "leveling"}` |

Copy these fields onto the existing Gantt task. **Don't add `leveling_delay` to the returned start again**, and don't stretch `duration` to cover a pause. `leveling_delay`, pause `at` and pause `length` use elapsed task units, where an elapsed day is 24 hours. Pause `at` counts from the task start. Duration, dependency lag and assignment offsets stay in working time.

For example, in hour units, `pauses: [{at: 4, length: 2, by: "leveling"}]` interrupts a task four elapsed hours after it starts, for two elapsed hours. The native pause renderer draws that interruption inside the returned bar bounds. The task keeps its ID, links and assignments.

This JavaScript fragment assumes you've initialized the WASM package, imported `Project`, loaded the tasks into the chart, and adapted `data` as described above:

```js
const project = new Project({ dates: "date" });
let applyingEngineUpdates = false;

// Application edit handlers should return early while this flag is true.
function displayResult(result) {
  applyingEngineUpdates = true;
  try {
    gantt.batchUpdate(() => {
      for (const update of result.updates) {
        const task = gantt.getTask(update.id);
        for (const field of [
          "start_date", "end_date", "duration", "leveling_delay", "pauses"
        ]) {
          if (Object.prototype.hasOwnProperty.call(update, field)) {
            task[field] = structuredClone(update[field]);
          }
        }
        gantt.updateTask(task.id);
      }
    });
  } finally {
    applyingEngineUpdates = false;
  }
}

displayResult(project.solve(data, { includeUnchanged: true }));

const leveled = project.level({
  order: "priority",
  allowSplits: true,
  withinSlackOnly: false,
  clearFirst: true
});
displayResult(leveled);
// Also persist the updates and display cycles, conflicts and overallocations.
```

Apply only the fields present in an update. Omitted fields are unchanged, but `pauses: []` and `leveling_delay: 0` must clear earlier leveling. Don't map result `kind: "summary"` onto Gantt's task `type`. The existing `"project"` type is still the right one. Dispose of the WASM project when the editor closes.

You can restrict leveling by resources, tasks and date range. The date range picks which contention to resolve; it isn't a deadline for where tasks end up. Use `withinSlackOnly` to limit how far the schedule extends, `allowSplits: false` for whole-task delays only, and task `priority: 1000` to protect a task from leveling.

Leveling only moves tasks forward and works at hour/day bucket granularity. `converged: true` doesn't guarantee zero overload. Remaining windows explain what blocked it, such as `fixed-task`, `pinned-constraint`, `completed`, `priority-1000`, `slack-exhausted` or `out-of-scope`. Show these reasons next to the affected tasks. Assignments that touch different parts of one bucket can still count together, so pick the basis that matches your planning precision.

For now, all pauses are leveling-owned. `clearFirst: true` clears in-scope pauses as well as delays, including pauses you supplied. There's no separate protected category for manual pauses. Leveling doesn't change assignment slices, and it doesn't preserve just the completed portion of a partially complete task.

### Preview edits and leveling

Use `preview(changes, {baselineId, profile})` for a drag, duration edit or reassignment that users should check before committing. It returns hypothetical dates and, optionally, a resource profile, without touching the accepted project. Show those dates in a comparison layer or a separate chart. On acceptance, apply the original changes and solve. A preview result isn't a committed baseline.

For a **Preview leveling** button, clone the project and call `level(options)` on the clone. Leave the live chart as is while you show the candidate result. If the user accepts and the source baseline is still current, run the same options on the live project and apply its updates. Dispose of the clone afterward. The engine doesn't store historical display baselines, so if you want before/after bars or undo, save the original task dates in application state.

### Resource load and diagnostics

`solve`, `preview` and `level` all report resource `overallocations`. Use their task IDs and time windows to mark the affected rows and open a conflict panel. `loadProfile({from, to, resources, basis, tasks})` gives you bucket load and capacity for the solved baseline; a preview can return the same profile for its hypothetical dates.

Feed that profile into Gantt's resource diagram/histogram templates or a panel next to the chart. Native task pause rendering doesn't wire Planning Engine's resource metrics into those views on its own. The engine profile already accounts for assignment slices, pauses and the selected calendar mode, so the load you show matches the plan. Keep chart bucket widths, rate units and profile basis aligned.

If you switch to Planning Engine's default `calendar_mode: "resource"` on purpose, each assignment can work on its own calendar, and the task spans their completion dates. You get richer resource planning, but you're no longer targeting Gantt's single-calendar scheduling parity. Use the engine's profile instead of deriving each person's load from the task bar around it. Results don't expose individual assignment start/end dates.

### Compare alternatives and suggest assignments

`evaluate(candidates, options)` compares change sets without changing the project. A Gantt application can offer alternatives, like moving a task, picking another assignee or adding capacity, and then show project finish, focused task finishes, overload, conflicts and cycles for each. Pass `level: {...}` to compare the alternatives after leveling too.

Evaluation returns metrics, not task updates. To show a candidate's dates, preview it. To accept it, apply its original change set and solve or level with the same options you used for scoring. The built-in cost is a scheduling heuristic. It isn't a financial cost, and it doesn't guarantee an optimal plan.

For an **Assign resources** action:

1. Keep worker skills and task requirements in application data, and convert them with `project.slots(pool, requirements)`. Or supply slots with explicit eligible resource IDs.
2. Call `suggestAssignments(slots, {targets: ["empty", "broken", "valid"]})` to fill empty roles, repair ineligible assignments and rebalance eligible ones. Pick only the targets your UI needs.
3. Show the proposal's `steps`, `unfilled` and `invalid` lists. It's a greedy suggestion, so some requirements may stay unresolved.
4. On acceptance, apply `proposal.changes`, check the apply report, then solve or level. Mirror accepted assignment changes into Gantt's assignment store/resource fields, and apply the task-date updates as well.

Use `fixed` assignment IDs to keep the people you've chosen, and `newResourcePenalty` to discourage pulling in more resources. If you scored suggestions with leveling, commit with the same leveling options. If an assignment changes the effective Gantt calendar, rebuild and validate that calendar mapping before you treat the proposal as final.

Adding people changes resource load. It doesn't shorten a task by itself. If your application uses effort-based duration rules, calculate the matching duration edit and include it explicitly.

### Critical path and slack

Request `analytics: {criticalPath: true, slack: true}` when you solve or preview, or use `analyze` to inspect dates without rescheduling them. Use the returned task/link IDs for highlighting and the slack values for grid columns or tooltips. Analytics on a cyclic graph are marked invalid.

These analytics aren't cross-validated against Gantt's [critical-path extension](https://docs.dhtmlx.com/gantt/guides/critical-path/). If your application shows Planning Engine's planning metrics, use its returned classifications consistently. Don't assume Gantt's own critical path will match.
