Plain objects and dates#
This guide covers what plen-js accepts as input and what it hands back: plain objects, field names, dates, numbers and ids. Read it before you connect the scheduler to your UI state, a server API or a database.
Plain objects in, plain objects out#
The library has no model classes to build. Every argument is a plain object or an array, and every result is a plain object you can read, change or throw away.
On the way in, the library reads your data the way JSON.stringify does:
toJSON()is called, so objects that define it (includingDate) work as values.- Fields set to
undefined, functions and symbols are dropped. Inside arrays, they becomenull. Number,StringandBooleanwrapper objects unwrap to their values.- Each getter is read once.
- Only own enumerable keys count. A
Mapor aSetarrives as an empty object. - Circular structures and
BigIntvalues throw aTypeError, as they do inJSON.stringify.
Your input is never mutated. You can pass objects straight from your UI store; the library copies what it needs.
On the way out, every call returns fresh objects that share nothing with your input or with the project's internal state. Keys come in sorted order. Changing a result can't affect later calls.
Record fields the library doesn't know are ignored, so a task can carry your own text, color or owner fields. Option objects and change sets are stricter: an unknown key there throws an InputError.
Field spelling#
Two naming styles meet in the API, and each has its own place:
| Where | Style | Examples |
|---|---|---|
Project data: tasks, links, calendars, resources, assignments, config |
snake_case |
start_date, duration_unit, constraint_type, leveling_delay, calendar_id |
| Options objects | camelCase |
includeUnchanged, baselineId, criticalPath, withinSlackOnly |
| Result envelopes and reports | camelCase |
baselineId, needsResync, capacityMode, linkId |
| Task fields inside results | snake_case |
start_date, end_date, leveling_delay |
Data fields are snake_case, and task fields in results use the same names, so an entry of updates works as a TaskUpdate.
A wrong spelling in an options object fails loudly:
const = new;
const = ;
A wrong spelling in a task record doesn't throw, because unknown record fields are ignored. A task with startDate instead of start_date looks like a task without a start date. Watch for validation errors that name a field you think you passed.
Assignments accept task_id and resource_id as aliases for task and resource.
Dates in#
Every date field takes a DateValue: a string or a Date. You can mix them in one call.
Strings use the YYYY-MM-DD form, with an optional time of day as HH:MM or HH:MM:SS, separated by a space or a T. A date without a time means midnight. Seconds are dropped.
Date objects are read from their local components: year, month, day, hour and minute in the timezone of the running code. Seconds and milliseconds are dropped. No timezone or UTC offset ever reaches the engine; all scheduling runs on naive local wall-clock time at minute precision.
const = new;
const = ;
;
// [ [ 1, '2026-01-05 08:30' ], [ 2, '2026-01-07 08:30' ], [ 3, '2026-01-05 09:15' ] ]
Task 1's Date becomes 2026-01-05 08:30: the 45 seconds are gone. Task 2 starts at midnight in the input and moves after task 1 through the link.
Bad dates throw an InputError with kind: "validation":
- an unparseable string, such as
"05/01/2026"(message"invalid date") - an invalid
Date, such asnew Date("x")(message"Invalid Date")
If your dates come as UTC ISO strings from a server, such as "2026-01-05T08:30:00Z", turn them into Date objects first. The engine has no timezone, so you decide which wall-clock time a task should show.
Dates out#
Result dates are strings in YYYY-MM-DD HH:MM form by default. Pass dates: "date" to the Project constructor to get local Date objects instead:
const = new;
const = ;
const = result..;
; // true
; // 8 30
The Date is built from the local components of the result, so getHours() returns the wall-clock hour the engine computed. The setting covers every date in results:
start_dateandend_dateinupdates- conflict dates (
required,actual,proposed,snapped) - overallocation windows (
from,to) - load profile buckets (
start), fromloadProfile()and frompreview()withprofile - finish dates in
evaluateandsuggestAssignmentsmetrics
The option is stored on the project. A clone() keeps it.
Set dates: "date" to get result dates as local Date objects instead of "YYYY-MM-DD HH:MM" strings; schedule() always returns strings.
An invalid value, such as dates: "iso", makes the constructor throw an InputError with path: "dates".
new Project(options) throws an InputError if an option is unknown or has an invalid value.
Pick the representation by what consumes the dates. Use strings when they go to a Gantt chart that parses "YYYY-MM-DD HH:MM", to a server or to storage. Use Date objects when your UI code does date math or formats dates with Intl.
Numbers and ids#
Every numeric field is a plain JS number: durations, lags, capacities, assignment values, progress, leveling_delay and pause offsets. Nothing uses bigint.
Numbers must be finite. NaN, Infinity or -Infinity anywhere in the input throws an InputError with kind: "validation", message "Numbers must be finite", and the field name in path:
The check runs before the data is decoded. That's why path holds just the field name, not the full tasks[0].progress path that other validation errors carry. -0 is read as 0.
Ids (Id) are strings or numbers, and results return each id in the type you passed in. Mixing types for the same entity is a trap: ids compare the way JS object keys do, so 7 and "7" are the same id. Two tasks with these ids fail with duplicate task id "7".
Numeric ids are JS numbers, so integers above Number.MAX_SAFE_INTEGER lose precision before they reach the engine. If your database uses 64-bit ids, pass them as strings.
Storing and sending results#
Results are plain data, so you can keep them, mutate them, structuredClone them or post them to a worker without extra steps.
With the default string dates, JSON.stringify(result) gives text you can store or send as is. Parsing it back gives the same object.
With dates: "date", be careful at the JSON boundary. JSON.stringify turns each Date into a UTC ISO string through Date#toJSON, so a task that starts at 08:30 local time can show up as "2026-01-05T07:30:00.000Z". Code that later reads only the date and time parts sees the wrong hour. For JSON, keep string dates: use a second project with the default dates, or format the Date objects yourself.
const = ;
;
Each entry in updates holds only the fields that changed, with the same names and value formats as the task data. You can merge it into your stored task records, or pass it back to the library as a TaskUpdate in a later change set.
Results with Date objects can also go straight back in as input, since every date field accepts a Date. The round trip keeps the wall-clock time.
For the full shape of a result, see ScheduleResult. For the input records, see ProjectData.