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:

  • 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 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 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.
Edit handlersignore eventswhile engineupdates arewrittensolve(data,includeUnchanged)normalized datesfirst displayedit a taskedit event (Ganttauto-scheduling isoff)apply(changes)apply reportsolve(null) orlevel(options)updates anddiagnosticsbatchUpdate withthe updatesYour adapterGanttUserPlanning EngineUserGanttYour adapterPlanning Engine

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:

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. If your application shows Planning Engine's planning metrics, use its returned classifications consistently. Don't assume Gantt's own critical path will match.