Project data#
This guide shows how to describe a plan for Planning Engine: tasks, links, ids, dates, durations, and the config switches that change how the plan is scheduled. It also lists what strict validation rejects, so you can build valid input the first time.
How project data works#
A plan goes in as one ProjectData value. It holds six lists: config, calendars, tasks, links, resources and assignments. Only tasks is required. The rest default to empty, and config defaults to SchedulingConfig::default().
ProjectData is an import format, not stored state. solve(Some(data)), schedule and analyze validate it and build the engine's own normalized model from it. The engine never hands the snapshot back. Results report changes as sparse updates, and you write them back into your own model.
The records are plain structs with public fields:
TaskData::new(id)gives a task with every default filled in. Set the fields you need on top of it.LinkData,CalendarData,ResourceDataandAssignmentDatahave no constructor. You write them as struct literals.ProjectDataandSchedulingConfigimplementDefault, so..Default::default()fills the rest.
Field names follow dhtmlxGantt (start_date, duration, constraint_type, auto_scheduling), so gantt data maps one-to-one. The two renames are task_type and link_type, which are called type on the wire.
A few rules hold everywhere:
Idis a number or a string and compares like a JavaScript object key, so task1and task"1"are the same task.DateTimeis a naive local date-time at minute precision. There's no time zone, no DST and no "today".durationandlagare working time, measured on calendars.leveling_delayandpausesare elapsed wall-clock time.- Strict validation (the default) rejects ambiguous input with a path-qualified
InputErrorinstead of guessing.Validation::Repairfixes what it can and reports each fix.
Input records have no Deserialize. To read JSON input, use the plen-wire crate, which decodes the same field names.
For every field and its default, see ProjectData, TaskData and SchedulingConfig.
Describing a project#
Tasks#
A leaf task needs two of start_date, end_date and duration. The engine derives the third. If you send all three, they must agree on the task's calendar.
use ;
The links push build after design and release after build, and only those two tasks appear in updates. The summary phase has no dates of its own, so its span comes from design and build.
Each task type has its own date rule:
TaskType::Task(the default) is a leaf: two of start, end and duration.TaskType::Summarytakes both dates or neither. Its span is derived, so a summary withstart_date+durationis rejected. A summary with no dates needs at least one dated child.TaskType::Milestoneneedsstart_dateorend_date. Its duration is 0.
parent links a task to its summary. None, "" and a value equal to config.root_id (default 0) all mean "no parent". A parent that doesn't exist, or a parent chain that loops back on itself, is a validation error.
The rest of TaskData covers constraints (constraint_type, constraint_date), calendars (calendar_id), and leveling inputs (priority, leveling_delay, pauses). See Links, constraints and summaries, Working time and calendars and Leveling resources.
Ids and dates#
Anything that converts into Id works where an id is expected: &str, String, f64 and i32. Ids compare by their JavaScript key, Id::key(), so a number and its string form are the same id.
use Id;
Keep this in mind when you mix sources: task 1 from one system and task "1" from another collide as duplicates. Numeric ids are f64, so integers past 2^53 lose precision just as they do in JavaScript.
DateTime is a local date-time with minute precision. Build one with DateTime::from_ymd_hm, which returns None for an impossible date, or convert from chrono::NaiveDateTime (seconds are dropped):
use ;
use DateTime;
The chrono conversions need chrono in your own Cargo.toml (cargo add chrono).
DateTime carries no time zone. Convert zoned timestamps to the plan's local time before you build the data. The engine never reads the clock, so an entirely undated project is rejected instead of being placed at "today".
Durations and units#
duration is working time in a unit: TimeUnit::Minute, TimeUnit::Hour or TimeUnit::Day. config.duration_unit sets the unit for the whole project (default Day). A task's own duration_unit overrides it for that task.
use ;
Durations are f64, so 1.5 days or 0.25 hours are valid. A negative or non-finite duration is rejected. Week, month and year units aren't supported. TimeUnit::parse("week") returns an error, and the enum has no such variant.
Day units count whole working days, the way dhtmlxGantt does. From a midnight anchor, Tuesday 00:00 + 4 days ends at Saturday 00:00, which is the close of Friday. From a non-midnight anchor, days are counted to the same time of day. With a sparse calendar that can skip working hours: on a Mon-Wed 9:00-13:00 calendar, a 3-day task from Monday 9:00 ends the next Monday 9:00. If that matters, use hour units.
Two task fields use elapsed time instead of working time: leveling_delay and pauses. Both are in the task's duration_unit, where a day is 24 hours, and both are rounded to whole minutes on use. The leveler writes them. Read Leveling resources before you set them by hand.
Links#
A LinkData connects a source task to a target task. link_type is one of LinkType::Fs (finish-to-start, the default), Ss, Ff or Sf. lag is working time added along the link, in config.duration_unit unless the link sets its own lag_unit.
use ;
A per-link lag_unit is a Planning Engine extension. dhtmlxGantt always uses the project unit for lag, so leave lag_unit as None when you need gantt-identical dates.
Some links can't apply. The engine drops them and reports a Note::DroppedLink with the reason instead of failing:
- the source or target task doesn't exist;
- the source or target task is unscheduled;
- the link joins a summary to one of its own descendants, and
config.descendant_linksisfalse.
A duplicate link id or a non-finite lag is a validation error. The engine keeps a link from a task to itself and reports it as a one-task cycle. How each link type moves its target, and how links attach to summaries, is in Links, constraints and summaries.
Fixed, unscheduled and completed tasks#
Three task states keep a task where it is:
auto_scheduling: falsemakes the task fixed. The planner never moves it, but links from it still push its successors. This flag replaces gantt's per-task cancellation events.unscheduled: truetakes the task out of the plan. It isn't scheduled or reported, and it doesn't need dates. Its descendants go with it. Links that touch it are dropped with a note.- With
config.use_progresson, a task withprogress == 1.0is completed and stays put.
use ;
progress must be between 0 and 1. A completed task also stays in place inside a summary that moves as a block. Here Planning Engine differs on purpose from dhtmlxGantt, which drags completed children along with the summary. In analytics, completed tasks have zero slack and are never critical.
Scheduling config#
SchedulingConfig holds the project-wide switches. Start from Default and override only what you need:
use ;
The switches you'll change most often:
| Field | Default | Effect |
|---|---|---|
apply_constraints |
false |
Turns on the eight constraint types. Off, constraints are ignored, including mso pins |
schedule_from_end |
false |
Backward (ALAP) planning. level refuses such projects |
gap_behavior |
Preserve |
Preserve keeps tasks at their own dates when links allow; Compress pulls every task to its earliest position |
move_projects |
true |
Summaries move as a block; false schedules their leaves one by one |
use_progress |
false |
Tasks with progress == 1 are completed and fixed |
project_start |
None |
Lower bound for the plan, and the date source for undated tasks in repair mode |
project_end |
None |
Upper bound, used by backward projects |
duration_unit |
Day |
Unit of duration and lag |
use_calendars |
true |
false makes every calendar flat 24/7 time |
calendar_mode |
Resource |
Which calendars place the work; Task / TaskInherited match gantt |
max_iterations |
8 |
Planner pass limit; must be at least 1 |
root_id |
Id::Num(0.0) |
The parent value that means "no parent" |
project_constraint, group_mode, descendant_links, strict_calendar and default_resource_capacity cover rarer cases. See SchedulingConfig for all of them.
root_id exists for gantt data whose virtual root isn't 0. The engine strips it on import, so stored tasks never point at the root. With a non-zero root_id, a task with id 0 is an ordinary parent.
If you load data from dhtmlxGantt, set use_calendars explicitly from gantt's work_time. Left out, Planning Engine schedules on calendars. Import from dhtmlxGantt covers the rest of the mapping.
Validation rules#
The default Validation::Strict mode rejects input that dhtmlxGantt would coerce or guess at. The call returns Error::Input with a path to the field, and a live Project keeps its previous state and cached result.
use ;
Strict import rejects:
| Input | Error path |
|---|---|
| Duplicate task, link, resource or calendar id | tasks[i].id, links[i].id, resources[i].id, calendars[i] |
| A leaf with fewer than two of start, end and duration | tasks[i].start_date |
| Start, end and duration that don't agree | tasks[i].duration |
end_date before start_date |
tasks[i].end_date |
| A summary with only one date, or start + duration | tasks[i].start_date |
| A summary with no dates and no dated children | tasks[i] |
| A milestone with no date | tasks[i].start_date |
| A negative or non-finite duration | tasks[i].duration |
A dated constraint without constraint_date |
tasks[i].constraint_date |
An unknown parent, or a cycle in the hierarchy |
tasks[i].parent |
progress outside 0..1, priority above 1000, negative leveling_delay |
tasks[i].progress, .priority, .leveling_delay |
Unsorted, overlapping or invalid pauses |
tasks[i].pauses[j] |
An unknown task calendar_id |
tasks[i].calendar_id |
| A calendar with no working time | calendars[i] |
A non-finite link lag |
links[i].lag |
max_iterations: 0 |
config.max_iterations |
Strict mode also rejects week, month and year units, and gantt's customWeeks calendars. Resource and assignment rules (capacity, value, slices, calendars with no shared working time) are in Resources and assignments.
Some things aren't errors. Links that can't apply become Note::DroppedLink, unscheduled tasks become Note::DroppedTask, and dependency cycles are reported in cycles. The wire decoder ignores unknown JSON fields.
When the data comes from a UI that saves messy records, switch to repair mode instead of fixing the input yourself:
use ;
Repair deduplicates ids (the last record wins and keeps the first position), derives missing dates and durations, lets dates win in an inconsistent triple, and clears dated constraints without a date. Each fix shows up as a Note::Repaired with its path. A task with no dates gets config.project_start, or the earliest date of another task. When neither exists, as in a fully undated project, repair fails at tasks[i].start_date. See Accept messy input and Error handling.