[Canonical page](<https://docs.dhtmlx.com/planning-engine/js/plain-objects-and-dates/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# 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 (including `Date`) work as values.
- Fields set to `undefined`, functions and symbols are dropped. Inside arrays, they become `null`.
- `Number`, `String` and `Boolean` wrapper objects unwrap to their values.
- Each getter is read once.
- Only own enumerable keys count. A `Map` or a `Set` arrives as an empty object.
- Circular structures and `BigInt` values throw a `TypeError`, as they do in `JSON.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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md>).

## 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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/task-update.md>).

A wrong spelling in an options object fails loudly:

```js
import { Project, InputError } from "plen-js";

const project = new Project();
const data = { tasks: [{ id: 1, start_date: "2026-01-05", duration: 2 }] };

try {
  project.solve(data, { include_unchanged: true });
} catch (error) {
  if (error instanceof InputError) {
    console.log(error.kind);    // "parse"
    console.log(error.message); // unknown field `include_unchanged`, expected one of ...
  }
}
```

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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/common.md#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.

```js
import { Project } from "plen-js";

const project = new Project();
const result = project.solve(
  {
    tasks: [
      { id: 1, start_date: new Date(2026, 0, 5, 8, 30, 45), duration: 2 },
      { id: 2, start_date: "2026-01-05", duration: 1 },
      { id: 3, start_date: "2026-01-05T09:15", duration: 1 },
    ],
    links: [{ id: 1, source: 1, target: 2 }],
  },
  { includeUnchanged: true },
);

console.log(result.updates.map((u) => [u.id, u.start_date]));
// [ [ 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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) with `kind: "validation"`:

- an unparseable string, such as `"05/01/2026"` (message `"invalid date"`)
- an invalid `Date`, such as `new 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`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>) constructor to get local `Date` objects instead:

```js
import { Project } from "plen-js";

const project = new Project({ dates: "date" });
const result = project.solve(
  { tasks: [{ id: 1, start_date: "2026-01-05 08:30", duration: 2 }] },
  { includeUnchanged: true },
);

const start = result.updates[0].start_date;
console.log(start instanceof Date);              // true
console.log(start.getHours(), start.getMinutes()); // 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_date` and `end_date` in `updates`
- conflict dates (`required`, `actual`, `proposed`, `snapped`)
- overallocation windows (`from`, `to`)
- load profile buckets (`start`), from [`loadProfile()`](<https://docs.dhtmlx.com/planning-engine/js/api/project-load-profile.md>) and from [`preview()`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) with `profile`
- finish dates in [`evaluate`](<https://docs.dhtmlx.com/planning-engine/js/api/project-evaluate.md>) and [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/js/api/project-suggest-assignments.md>) metrics

The option is stored on the project. A [`clone()`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md#clone>) keeps it.

:::note
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"`.

:::note
`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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) with `kind: "validation"`, message `"Numbers must be finite"`, and the field name in `path`:

```js
import { Project, InputError } from "plen-js";

try {
  new Project().solve({
    tasks: [{ id: 1, start_date: "2026-01-05", duration: 1, progress: Number.NaN }],
  });
} catch (error) {
  if (error instanceof InputError) {
    console.log(error.message, error.path); // "Numbers must be finite" "progress"
  }
}
```

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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/common.md#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.

```js
import { Project } from "plen-js";

const data = {
  tasks: [
    { id: 1, start_date: "2026-01-05 08:30", duration: 2 },
    { id: 2, start_date: "2026-01-05 08:30", duration: 1 },
  ],
  links: [{ id: 1, source: 1, target: 2 }],
};

async function main() {
  const project = new Project(); // string dates
  const { updates } = project.solve(data);

  await fetch("/api/tasks/batch", {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(updates),
    // [{"end_date":"2026-01-08 08:30","id":2,"kind":"task","start_date":"2026-01-07 08:30"}]
  });
}

main();
```

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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/task-update.md>) 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`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md>). For the input records, see [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/js/api/types/project-data.md>).
