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_idtotask/resource(the wire format also accepts the original names). - For
default, mapdelayto working-timeoffsetand leave the duration open to the task end. ForfixedDuration, copydurationtoo. - For
fixedDates, calculate the working-timeoffsetanddurationthat 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
modeas an engine setting. Planning Engine has no assignment mode and clips all slices at the task end, while Gantt'sfixedDurationcan extend past it. - Normalize assignment
valueand resourcecapacityto the same rate scale. Planning Engine uses1for 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,houranddayare supported.week,month,yearand calendarcustomWeeksare rejected, includingcustomWeeksinside 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,notesandconverged. 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:
- Import the complete planning document with
solve(data, {includeUnchanged: true}). You get normalized dates for the first display. Keep the sourceleveling_delayandpauses, though: a plain solve doesn't emit those fields when they're unchanged. - Turn a user edit into
apply(changes), check its report, then callsolve(null)orlevel(options). - 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.
- Use the latest
baselineIdwhen you accept a preview or proposal. IfapplyreportsneedsResync, reconcile the source data and replace the engine state withsolve(data)before you continue. Some peer operations may have already been applied.
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 = new;
let = false;
// Application edit handlers should return early while this flag is true.
;
const = ;
;
// 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:
- 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. - 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. - Show the proposal's
steps,unfilledandinvalidlists. It's a greedy suggestion, so some requirements may stay unresolved. - 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.