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 object. It holds six fields: config, calendars, tasks, links, resources and assignments. Only tasks is required. The lists start empty, and config starts as new SchedulingConfig().
ProjectData is an import format, not stored state. Project#solve, Engine.schedule and Project#analyze validate it and build the engine's own normalized model from it. solve deep-copies the object first, so changing it afterwards doesn't touch the project. 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 mutable classes with public fields and a fluent withX(value) setter per field:
new TaskData(id)gives a task with every default filled in. Chain the setters you need on top of it.LinkData,CalendarData,ResourceDataandAssignmentDatahave a no-argument constructor. Set their fields withwithX.ProjectDataandSchedulingConfigstart with every default, so you set only what differs.- List fields are
Vec<T>, anArrayList<T>. Every list setter also takes ajava.util.Collection, sowithTasks(List.of(...))works.
Field names are camelCase (startDate, duration, constraintType, autoScheduling). In JSON they are snake_case (start_date, constraint_type), and taskType / linkType are called type. To load JSON, call Decode.fromJson(json, ProjectData.class). Unknown JSON fields in the data are ignored.
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.Momentis a naive local date-time at minute precision. There's no time zone, no DST and no "today".durationandlagare working time, measured on calendars.levelingDelayandpausesare elapsed wall-clock time.- An optional field is
nullwhen absent (Double duration,Moment startDate,Id parent). - Strict validation (the default) rejects ambiguous input with a path-qualified
InputExceptioninstead of guessing.Validation.REPAIRfixes what it can and reports each fix.
For every field and its default, see ProjectData, TaskData and SchedulingConfig.
Describing a project#
Tasks#
A leaf task needs two of startDate, endDate and duration. The engine derives the third. If you send all three, they must agree on the task's calendar.
;
;
The links push build after design and release after build, and only those two tasks appear in updates. Both links use the default LinkType.FS with no lag. 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 withstartDate+durationis rejected. A summary with no dates needs at least one dated child.TaskType.MILESTONEneedsstartDateorendDate. Its duration is 0.
parent links a task to its summary. null, Id.str("") and a value equal to config.rootId (default Id.num(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 (constraintType, constraintDate), calendars (calendarId), and leveling inputs (priority, levelingDelay, pauses). See Links, constraints and summaries, Working time and calendars and Leveling resources.
Ids and dates#
Build an Id with Id.num(double) or Id.str(String). Ids compare by their JavaScript key, Id#key(), so a number and its string form are the same id. equals, hashCode and toString all follow the key.
;
This matters when you mix sources. Task 1 from one system and task "1" from another collide as duplicates. Numeric ids are double, so integers past 2^53 lose precision just as they do in JavaScript.
Moment is a local date-time with minute precision. There are three ways to build one:
Moment.fromYmdHm(...)returnsnullfor an impossible date.Moment.fromDateTime(LocalDateTime)drops seconds.Moment.parse(String)readsYYYY-M-Dwith an optional time. It throwsIllegalArgumentExceptionfor bad input.
;
;
Moment carries no time zone. Convert zoned timestamps to the plan's local wall-clock 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". Records, ids and dates covers both types in more depth.
Durations and units#
duration is working time in a unit: TimeUnit.MINUTE, TimeUnit.HOUR or TimeUnit.DAY. config.durationUnit sets the unit for the whole project (default DAY). A task's own durationUnit overrides it for that task.
;
;
Durations are Double, so 1.5 days or 0.25 hours are valid. A negative or non-finite duration is rejected. TimeUnit has no week, month or year, and Decode rejects those spellings in JSON with unknown duration unit.
Day units count whole working days. 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: levelingDelay and pauses. Both are in the task's durationUnit, 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. linkType is one of LinkType.FS (finish-to-start, the default), SS, FF or SF. lag is working time added along the link, in config.durationUnit unless the link sets its own lagUnit.
;
;
Leave lagUnit as null to measure lag in the project's durationUnit.
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.descendantLinksisfalse.
A duplicate link id or a non-finite lag is a validation error. A link from a task to itself stays in the plan and shows up 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:
autoScheduling = falsemakes the task fixed. The planner never moves it, but links from it still push its successors.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.useProgresson, a task withprogress == 1.0is completed and stays put.
;
;
progress must be between 0 and 1. A completed task also stays in place inside a summary that moves as a block. In analytics, completed tasks have zero slack and are never critical.
Scheduling config#
SchedulingConfig holds the project-wide switches. Start from new SchedulingConfig() and override only what you need:
;
;
The switches you'll change most often:
| Field | Default | Effect |
|---|---|---|
applyConstraints |
false |
Turns on the eight constraint types. Off, constraints are ignored, including MSO pins |
scheduleFromEnd |
false |
Backward (ALAP) planning. level refuses such projects |
gapBehavior |
PRESERVE |
PRESERVE keeps tasks at their own dates when links allow; COMPRESS pulls every task to its earliest position |
moveProjects |
true |
Summaries move as a block; false schedules their leaves one by one |
useProgress |
false |
Tasks with progress == 1 are completed and fixed |
projectStart |
null |
Lower bound for the plan, and the date source for undated tasks in repair mode |
projectEnd |
null |
Upper bound, used by backward projects |
durationUnit |
DAY |
Unit of duration and lag |
useCalendars |
true |
false makes every calendar flat 24/7 time |
calendarMode |
RESOURCE |
Which calendars place the work; TASK / TASK_INHERITED use task calendars only |
maxIterations |
8 |
Planner pass limit; must be at least 1 |
rootId |
Id.num(0) |
The parent value that means "no parent" |
projectConstraint, groupMode, descendantLinks, strictCalendar and defaultResourceCapacity cover rarer cases. See SchedulingConfig for all of them.
rootId exists for data whose virtual root isn't 0. The engine clears it on import, so stored tasks never point at the root. ProjectData#canonicalizeRoots() does the same on your own object. With a non-zero rootId, a task with id 0 is an ordinary parent.
Left out, useCalendars is on and Planning Engine schedules on calendars. Set it explicitly when your data shouldn't use them.
Validation rules#
The default Validation.STRICT mode rejects input it would have to coerce or guess at. The call throws an InputException with a path to the field, and a live Project keeps its previous state and cached result. InputException is unchecked, so catch it where you want to report the error.
;
;
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 |
endDate before startDate |
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 constraintDate |
tasks[i].constraint_date |
An unknown parent, or a cycle in the hierarchy |
tasks[i].parent |
progress outside 0..1, priority above 1000, negative levelingDelay |
tasks[i].progress, .priority, .leveling_delay |
Unsorted, overlapping or invalid pauses |
tasks[i].pauses[j] |
An unknown task calendarId |
tasks[i].calendar_id |
| A calendar with no working time | calendars[i] |
worktime.days with other than 7 entries |
calendars[i].worktime.days |
A non-finite link lag |
links[i].lag |
maxIterations = 0 |
config.max_iterations |
Paths use the JSON field names, so the same path points at the right field whether the data came from withX setters or from JSON. 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.
When the data comes from a UI that saves messy records, switch to repair mode instead of fixing the input yourself:
;
;
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.projectStart, 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.