[Canonical page](<https://docs.dhtmlx.com/planning-engine/net/scheduling/project-data/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# 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 has six properties: [`Config`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md#config>), [`Calendars`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md#calendars>), [`Tasks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md#tasks>), [`Links`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md#links>), [`Resources`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md#resources>) and [`Assignments`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md#assignments>). Every list starts empty, and `Config` starts as `new SchedulingConfig()` with its defaults.

`ProjectData` is an import format, not stored state. [`Project.Solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>), [`Engine.Schedule`](<https://docs.dhtmlx.com/planning-engine/net/api/engine-schedule.md>) and [`Project.Analyze`](<https://docs.dhtmlx.com/planning-engine/net/api/project-analyze.md>) validate it and build the engine's own normalized model from it. `Solve` deep-copies your object, so later changes to it don't leak into the project. The engine never hands the snapshot back. Results report changes as sparse [`Updates`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#updates>), and you write them back into your own model.

The records are mutable classes with `{ get; set; }` properties:

- [`new TaskData(id)`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/task-data.md>) gives a task with every default filled in. Set the properties you need in an object initializer.
- [`LinkData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md>), [`CalendarData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/calendar-data.md>), [`ResourceData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/resource-data.md>) and [`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/assignment-data.md>) have `required` members. Create them with object initializers, for example `new LinkData { Id = "l1", Source = "a", Target = "b" }`.
- `ProjectData` and `SchedulingConfig` have parameterless constructors, so you set only what differs from the defaults.
- Every record has [`DeepClone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/i-deep-cloneable.md#deepclone>) for a copy that shares no lists with the original.

Property names are the PascalCase forms of the wire field names (`StartDate`, `Duration`, `ConstraintType`, `AutoScheduling`). [`TaskType`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/task-data.md#tasktype>) and [`LinkType`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#linktype-1>) are called `type` on the wire.

A few rules hold everywhere:

- [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>) is a number or a string and compares like a JavaScript object key, so task `1` and task `"1"` are the same task.
- [`Moment`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md>) is a naive local date-time at minute precision. There's no time zone, no DST and no "today".
- `Duration` and `Lag` are working time, measured on calendars. `LevelingDelay` and `Pauses` are elapsed wall-clock time.
- Strict validation (the default) rejects ambiguous input with an [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) that names the field, instead of guessing. [`Validation.Repair`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/validation.md>) fixes what it can and reports each fix.

The core package doesn't read JSON. The separate `Plen.Wire` package decodes JSON with snake_case field names; see [Exchange JSON with a client](<https://docs.dhtmlx.com/planning-engine/net/how-to/exchange-json-with-a-client.md>).

For every property and its default, see [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md>), [`TaskData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/task-data.md>) and [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md>).

## 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.

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        // A summary with no dates: its span comes from its children.
        new TaskData("phase") { TaskType = TaskType.Summary },
        new TaskData("design") { Parent = "phase", StartDate = monday, Duration = 3 },
        new TaskData("build") { Parent = "phase", StartDate = monday, Duration = 5 },
        // A milestone needs only one date.
        new TaskData("release") { TaskType = TaskType.Milestone, StartDate = monday },
    ],
    Links =
    [
        new LinkData { Id = "l1", Source = "design", Target = "build" },
        new LinkData { Id = "l2", Source = "build", Target = "release" },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var update in result.Updates)
{
    if (update.StartDate is { } start && update.EndDate is { } end)
        Console.WriteLine($"{update.Id}: {start} .. {end}");
}
```

The links push `build` after `design` and `release` after `build`, and only those two tasks appear in [`Updates`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#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`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/task-data.md#tasktype>) (the default) is a leaf: two of start, end and duration.
- `TaskType.Summary` takes both dates or neither. Its span is derived, so a summary with `StartDate` + `Duration` is rejected. A summary with no dates needs at least one dated child.
- `TaskType.Milestone` needs `StartDate` or `EndDate`. Its duration is 0.

`Parent` links a task to its summary. `null`, `""` and a value equal to [`Config.RootId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#rootid>) (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`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/task-data.md>) covers constraints (`ConstraintType`, `ConstraintDate`), calendars (`CalendarId`), and leveling inputs (`Priority`, `LevelingDelay`, `Pauses`). See [Links, constraints and summaries](<https://docs.dhtmlx.com/planning-engine/net/scheduling/links-constraints-summaries.md>), [Working time and calendars](<https://docs.dhtmlx.com/planning-engine/net/scheduling/working-time-calendars.md>) and [Leveling resources](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md>).

### Ids and dates

`string`, `int` and `double` convert to [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>) implicitly, so you can write `"a"` or `1` wherever an id is expected. Ids compare by their JavaScript key, [`Id.Key()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md#key>), so a number and its string form are the same id.

```csharp
using Plen;

Id numeric = 1;
Id text = "1";
Console.WriteLine(numeric == text); // True

// -0 and 0 share the key "0".
Console.WriteLine(Id.Num(-0.0) == Id.Str("0")); // True

Console.WriteLine(numeric); // prints the key: 1
```

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`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md>) is a local date-time with minute precision, stored as minutes since 1970-01-01 00:00. Build one with [`Moment.FromYmdHm`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md#fromymdhm>), which returns `null` for an impossible date, or convert a `DateTime` with [`Moment.FromDateTime`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md#fromdatetime>) (seconds are dropped):

```csharp
using Plen;

var a = Moment.FromYmdHm(2026, 3, 2, 9, 30) ?? throw new InvalidOperationException("invalid date");
var b = Moment.FromDateTime(new DateTime(2026, 3, 2, 9, 30, 45)); // 09:30:45 becomes 09:30
Console.WriteLine(a == b); // True

Console.WriteLine(a); // 2026-03-02 09:30
DateTime back = a.ToDateTime(); // DateTimeKind.Unspecified
Console.WriteLine(back);
```

`Moment` carries no time zone. `FromDateTime` reads the date and time fields as they are and ignores `DateTime.Kind`, so convert a UTC value to the plan's local time first. The engine never reads the clock, so an entirely undated project is rejected instead of being placed at "today". [`ToDateTime()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md#todatetime>) throws `ArgumentOutOfRangeException` for a moment outside years 1-9999. [Records, ids and dates](<https://docs.dhtmlx.com/planning-engine/net/records-ids-and-dates.md>) covers both types in more depth.

### Durations and units

`Duration` is working time in a unit: [`TimeUnit.Minute`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/time-unit.md>), `TimeUnit.Hour` or `TimeUnit.Day`. [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>) sets the unit for the whole project (default `Day`). A task's own `DurationUnit` overrides it for that task.

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Config = new SchedulingConfig { DurationUnit = TimeUnit.Hour },
    Tasks =
    [
        // 12 working hours: the default calendar works 08-12 and 13-17.
        new TaskData("review") { StartDate = monday, Duration = 12 },
        // This task counts in minutes instead of the project's hours.
        new TaskData("sync") { StartDate = monday, Duration = 90, DurationUnit = TimeUnit.Minute },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
Console.WriteLine($"{result.Updates.Count} updates");
```

Durations are `double`, 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` has no such members.

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 properties use elapsed time instead of working time: `LevelingDelay` 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](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md>) before you set them by hand.

### Links

A [`LinkData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md>) connects a [`Source`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#source-target>) task to a `Target` task. [`LinkType`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#linktype>) is one of `LinkType.Fs` (finish-to-start, the default), `Ss`, `Ff` or `Sf`. [`Lag`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#lag>) is working time added along the link, in [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>) unless the link sets its own [`LagUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#lagunit>).

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 8, 0) ?? throw new InvalidOperationException("invalid date");
TaskData Make(string id) => new(id) { StartDate = monday, Duration = 2 };

var data = new ProjectData
{
    Tasks = [Make("a"), Make("b"), Make("c")],
    Links =
    [
        // b starts 1 working day after a starts.
        new LinkData { Id = "ss", Source = "a", Target = "b", LinkType = LinkType.Ss, Lag = 1 },
        // c finishes 4 working hours after b finishes.
        new LinkData
        {
            Id = "ff", Source = "b", Target = "c",
            LinkType = LinkType.Ff, Lag = 4, LagUnit = TimeUnit.Hour,
        },
        // Points at a task that doesn't exist: dropped, not an error.
        new LinkData { Id = "stray", Source = "c", Target = "missing" },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var note in result.Notes)
{
    if (note is Note.DroppedLink dropped)
        Console.WriteLine($"link {dropped.Link} dropped: {dropped.Reason}");
}
```

Leave `LagUnit` as `null` to measure lag in the project unit.

Some links can't apply. The engine drops them and reports a [`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md>) with the reason instead of throwing:

- 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.DescendantLinks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#descendantlinks>) is `false`.

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](<https://docs.dhtmlx.com/planning-engine/net/scheduling/links-constraints-summaries.md>).

### Fixed, unscheduled and completed tasks

Three task states keep a task where it is:

- `AutoScheduling = false` makes the task fixed. The planner never moves it, but links from it still push its successors.
- `Unscheduled = true` takes 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.UseProgress`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#useprogress>) on, a task with `Progress == 1` is completed and stays put.

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Config = new SchedulingConfig { UseProgress = true },
    Tasks =
    [
        // Completed, because UseProgress is on.
        new TaskData("done") { StartDate = monday, Duration = 2, Progress = 1 },
        // Fixed.
        new TaskData("pinned") { StartDate = monday, Duration = 1, AutoScheduling = false },
        // No dates needed.
        new TaskData("later") { Unscheduled = true },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var note in result.Notes)
{
    if (note is Note.DroppedTask dropped)
        Console.WriteLine($"task {dropped.Task}: {dropped.Reason}"); // task later: unscheduled
}
```

`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. Create it with `new` and set only what you need:

```csharp
using Plen;

var kickoff = Moment.FromYmdHm(2026, 3, 2, 8, 0) ?? throw new InvalidOperationException("invalid date");

var config = new SchedulingConfig
{
    ApplyConstraints = true,              // constraints act as hard bounds
    GapBehavior = GapBehavior.Compress,   // pull tasks to their earliest
    DurationUnit = TimeUnit.Hour,
    ProjectStart = kickoff,               // nothing starts before this
    CalendarMode = CalendarMode.Task,     // task calendars only
    RootId = -1,                          // data with a virtual root of -1
};

var data = new ProjectData
{
    Config = config,
    Tasks =
    [
        // Parent equals RootId: no parent.
        new TaskData(1) { Parent = -1, StartDate = kickoff, Duration = 4 },
    ],
};

Engine.Schedule(data, new SolveOptions());
```

The switches you'll change most often:

| Property | Default | Effect |
|---|---|---|
| [`ApplyConstraints`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#applyconstraints>) | `false` | Turns on the eight constraint types. Off, constraints are ignored, including `Mso` pins |
| [`ScheduleFromEnd`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#schedulefromend>) | `false` | Backward (ALAP) planning. [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) refuses such projects |
| [`GapBehavior`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#gapbehavior>) | `Preserve` | `Preserve` keeps tasks at their own dates when links allow; `Compress` pulls every task to its earliest position |
| [`MoveProjects`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#moveprojects>) | `true` | Summaries move as a block; `false` schedules their leaves one by one |
| [`UseProgress`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#useprogress>) | `false` | Tasks with `Progress == 1` are completed and fixed |
| [`ProjectStart`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#projectstart>) | `null` | Lower bound for the plan, and the date source for undated tasks in repair mode |
| [`ProjectEnd`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#projectend>) | `null` | Upper bound, used by backward projects |
| [`DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>) | `Day` | Unit of `Duration` and `Lag` |
| [`UseCalendars`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#usecalendars>) | `true` | `false` makes every calendar flat 24/7 time |
| [`CalendarMode`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#calendarmode>) | `Resource` | Which calendars place the work; `Task` / `TaskInherited` use task calendars only |
| [`MaxIterations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#maxiterations>) | `8` | Planner pass limit; must be at least 1 |
| [`RootId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#rootid>) | `Id.Num(0.0)` | The `Parent` value that means "no parent" |

[`ProjectConstraint`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#projectconstraint>), [`GroupMode`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#groupmode>), [`DescendantLinks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#descendantlinks>), [`StrictCalendar`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#strictcalendar>) and [`DefaultResourceCapacity`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#defaultresourcecapacity>) cover rarer cases. See [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md>) for all of them.

`RootId` exists for 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 `RootId`, a task with id `0` is an ordinary parent.

Left at its default, `UseCalendars` is `true` and Planning Engine schedules on calendars. Set it explicitly when your data source has its own working-time switch.

### Validation rules

The default [`Validation.Strict`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/validation.md>) mode rejects ambiguous or inconsistent input. The call throws an [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) whose [`Path`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md#path>) names the field, and a live [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) keeps its previous state and cached result.

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("ok") { StartDate = monday, Duration = 1 },
        new TaskData("broken") { Duration = 2 }, // only one of start / end / duration
    ],
};

try
{
    Engine.Schedule(data, new SolveOptions());
    Console.WriteLine("scheduled");
}
catch (InputException e)
{
    Console.WriteLine($"{e.Path ?? "-"}: {e.Message}");
}
```

`Path` uses the wire spelling of the field (`tasks[1].start_date`), so it matches the JSON your client sent. 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`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/worktime.md#days>) without exactly 7 entries | `calendars[i].worktime.days` |
| A non-finite link `Lag` | `links[i].lag` |
| `MaxIterations = 0` | `config.max_iterations` |
| A negative or non-finite `DefaultResourceCapacity` | `config.default_resource_capacity` |

Resource and assignment rules (capacity, `Value`, slices, calendars with no shared working time) are in [Resources and assignments](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/resources-and-assignments.md>).

Some things aren't errors. Links that can't apply become [`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md>), unscheduled tasks become [`Note.DroppedTask`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md#droppedtask>), and dependency cycles are reported in [`Cycles`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#cycles>).

When the data comes from a UI that saves messy records, switch to repair mode instead of fixing the input yourself:

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 8, 0) ?? throw new InvalidOperationException("invalid date");

var project = new Project(new ProjectOptions { Validation = Validation.Repair });
var data = new ProjectData
{
    Tasks =
    [
        new TaskData("a") { StartDate = monday, Duration = 1 },
        // Same id again: in repair mode the last record wins.
        new TaskData("a") { StartDate = monday, Duration = 3 },
    ],
};

var result = project.Solve(data, new SolveOptions());
foreach (var note in result.Notes)
{
    if (note is Note.Repaired repaired)
        Console.WriteLine($"{repaired.Path}: {repaired.Message}");
}
```

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`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md#repaired>) with its path. A task with no dates gets [`Config.ProjectStart`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#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](<https://docs.dhtmlx.com/planning-engine/net/how-to/accept-messy-input.md>) and [Error handling](<https://docs.dhtmlx.com/planning-engine/net/error-handling.md>).
