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


# Quick start

By the end of this page, you'll have a C# program that schedules two linked tasks, and a live project that reschedules them when one changes.

## Requirements

- .NET 10. The package targets `net10.0` and has nullable reference type annotations.
- No async setup. Every call is synchronous and CPU-only, with no I/O and no clock.

## Install

The packages come from a private NuGet feed at `git.dhtmlx.com`. With your license or trial you received a username and a password for one of two editions:

| Edition | Feed URL | Packages |
| --- | --- | --- |
| Full | `https://git.dhtmlx.com/api/packages/plen/nuget/index.json` | `Plen.Core`, `Plen.Assign`, `Plen.Wire` |
| Trial | `https://git.dhtmlx.com/api/packages/plen-trial/nuget/index.json` | `Plen.Core.Trial`, `Plen.Assign.Trial`, `Plen.Wire.Trial` |

The trial is the full engine with one limit: a project with more than 50 tasks is rejected with an [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>). Only the package ids differ. The assemblies, namespaces and types are the same, so the code in these docs runs on both.

Each account sees only its own edition. Trial credentials see only the `.Trial` packages on the `plen-trial` feed.

### Configure access to the feed

The examples use the full feed. For the trial, replace `plen` with `plen-trial` in the URL.

**Option A: `nuget.config` in your solution (recommended).** Create a `nuget.config` next to your solution file (`.sln` or `.slnx`), or next to the project if you have no solution:

```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="dhtmlx" value="https://git.dhtmlx.com/api/packages/plen/nuget/index.json" />
  </packageSources>

  <packageSourceCredentials>
    <dhtmlx>
      <add key="Username" value="%DHTMLX_NUGET_USER%" />
      <add key="ClearTextPassword" value="%DHTMLX_NUGET_PASSWORD%" />
    </dhtmlx>
  </packageSourceCredentials>

  <!-- Optional: take Plen.* only from the DHTMLX feed. -->
  <packageSourceMapping>
    <packageSource key="nuget.org">
      <package pattern="*" />
    </packageSource>
    <packageSource key="dhtmlx">
      <package pattern="Plen.*" />
    </packageSource>
  </packageSourceMapping>
</configuration>
```

The username and password come from environment variables, so the file can go into source control. Set them in your shell, and as secrets on your CI server:

```bash
# bash / zsh
export DHTMLX_NUGET_USER=your-username
export DHTMLX_NUGET_PASSWORD=your-password
```

```powershell
# PowerShell
$env:DHTMLX_NUGET_USER = "your-username"
$env:DHTMLX_NUGET_PASSWORD = "your-password"
```

**Option B: register the feed once per machine.** This writes the source to your user-level NuGet config instead of the solution:

```bash
dotnet nuget add source https://git.dhtmlx.com/api/packages/plen/nuget/index.json \
  --name dhtmlx \
  --username your-username \
  --password your-password \
  --store-password-in-clear-text
```

`--store-password-in-clear-text` is required on macOS and Linux, where NuGet can't encrypt passwords. On Windows, leave it out and the password is stored encrypted.

**Visual Studio and Rider** read the same `nuget.config`. Without one, add the feed in the IDE. In Visual Studio, open *Tools → Options → NuGet Package Manager → Package Sources*, add the feed URL, and enter the username and password when asked. In Rider, open *Settings → Build, Execution, Deployment → NuGet → Sources* and enter the feed URL, username and password.

### Add the package

Create a console app and add the package:

```bash
dotnet new console -o PlanDemo
cd PlanDemo
dotnet add package Plen.Core          # trial: Plen.Core.Trial
```

Everything on this page lives in the `Plen` namespace, so one `using Plen;` is enough. Two other packages exist, and you don't need either one here. [`Plen.Assign`](<https://docs.dhtmlx.com/planning-engine/net/api/assign/overview.md>) adds skill matching for auto-assignment. `Plen.Wire` reads and writes the JSON format, and pulls in `Plen.Core` and `Plen.Assign`. With the trial, add the `.Trial` id wherever these docs name a package.

### Move from the trial to the full edition

1. Change the feed URL from `.../plen-trial/...` to `.../plen/...` and use your full-edition credentials.
2. Swap the package references and keep the version:

   ```bash
   dotnet remove package Plen.Core.Trial
   dotnet add package Plen.Core
   ```

   Do the same for `Plen.Wire.Trial` and `Plen.Assign.Trial` if you use them. The code doesn't change.

### If the restore fails

- **`401 Unauthorized`:** the username or password is wrong, or the variables aren't set in the shell that runs `dotnet`. With two-factor authentication, use an access token as the password.
- **`Unable to find package Plen.Core`:** the feed and the package don't match. Trial credentials see only the `.Trial` packages. Also check that the URL ends in `/nuget/index.json`.
- **Changed credentials aren't picked up:** run `dotnet nuget locals http-cache --clear`.

## Schedule a snapshot

Start with the smallest useful plan: two tasks and a finish-to-start link. Both tasks start on Monday, 2026-08-10, at 08:00, so task 2 starts too early. It has to wait for task 1 to finish.

Replace `Program.cs` with this:

```csharp
using Plen;

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

var data = new ProjectData
{
    Tasks =
    [
        new TaskData(1) { StartDate = monday, Duration = 2 },
        new TaskData(2) { StartDate = monday, Duration = 1 },
    ],
    Links =
    [
        new LinkData { Id = 1, Source = 1, Target = 2, LinkType = LinkType.Fs },
    ],
};

try
{
    var result = Engine.Schedule(data, new SolveOptions());
    foreach (var update in result.Updates)
    {
        Console.WriteLine($"task {update.Id}: {update.StartDate} -> {update.EndDate}");
    }
}
catch (InputException e)
{
    Console.Error.WriteLine($"invalid input at {e.Path}: {e.Message}");
    return 1;
}
return 0;
```

Run it with `dotnet run`:

```
task 2: 2026-08-12 08:00 -> 2026-08-13 08:00
```

[`Engine.Schedule`](<https://docs.dhtmlx.com/planning-engine/net/api/engine-schedule.md>) takes a [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md>) snapshot and returns a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md>). Its [`Updates`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#updates>) list holds only the tasks that moved, as [`Mutation`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/mutation.md>) records. Task 1 didn't move, so it's not in the list.

Durations are working time in days. The default calendar works Monday to Friday, 08:00-12:00 and 13:00-17:00. That's why the two-day task 1 ends on Wednesday at 08:00, not on Tuesday evening.

Dates are [`Moment`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md>) values: local time, minute precision, no time zone. [`Moment.FromYmdHm`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md>) returns `Moment?` and gives `null` for an invalid date, so the example throws instead of reading `.Value` blindly. Ids are [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>) values. An `int`, a `double` or a `string` converts to `Id` on its own, and `1` and `"1"` name the same task. [Records, ids and dates](<https://docs.dhtmlx.com/planning-engine/net/records-ids-and-dates.md>) covers these types in detail.

### When the input is invalid

Every leaf task needs two of `StartDate`, `EndDate` and `Duration`. Remove task 2's start date and keep only its duration:

```csharp
new TaskData(2) { Duration = 1 },
```

[`Engine.Schedule`](<https://docs.dhtmlx.com/planning-engine/net/api/engine-schedule.md>) now rejects the snapshot before it schedules anything, and the `catch` block prints:

```
invalid input at tasks[1].start_date: task needs two of start_date / end_date / duration
```

The [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) has a [`Path`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md#path>) that points to the field that failed. The path uses the JSON field names, so `StartDate` shows up as `start_date`. Add the start date back and the program runs again.

Bad input is the only thing that makes this call throw. Dependency cycles, constraint conflicts and dropped links come back as data in [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md>) ([`Cycles`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#cycles>), [`Conflicts`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#conflicts>), [`Notes`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#notes>)), and the call still returns normally.

## Keep a live project

[`Engine.Schedule`](<https://docs.dhtmlx.com/planning-engine/net/api/engine-schedule.md>) fits batch jobs and request handlers that see each snapshot once. An editor backend works differently. It loads the plan once, then sends small changes as the user works. That's what [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>) does.

This program loads the same two tasks, previews a change, then commits it:

```csharp
using Plen;

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

var data = new ProjectData
{
    Tasks =
    [
        new TaskData(1) { StartDate = monday, Duration = 2 },
        new TaskData(2) { StartDate = monday, Duration = 1 },
    ],
    Links =
    [
        new LinkData { Id = 1, Source = 1, Target = 2, LinkType = LinkType.Fs },
    ],
};

var project = new Project();
var first = project.Solve(data, new SolveOptions());
Console.WriteLine($"loaded, {first.Updates.Count} task(s) moved");

// The user stretches task 1 to three days.
var changes = new SchedulingChanges();
changes.Tasks.Update.Add(new TaskUpdate(1) { Duration = 3 });

var preview = project.Preview(changes, new PreviewOptions());
Console.WriteLine($"preview: {preview.Updates.Count} task(s) would move");

var report = project.Apply(changes);
if (report.NeedsResync)
{
    Console.Error.WriteLine("an edit was rejected; reload the project with Solve(data, ...)");
    return 1;
}

var result = project.Solve(null, new SolveOptions());
foreach (var update in result.Updates)
{
    Console.WriteLine($"task {update.Id}: {update.StartDate} -> {update.EndDate}");
}
return 0;
```

Output:

```
loaded, 1 task(s) moved
preview: 2 task(s) would move
task 1: 2026-08-10 08:00 -> 2026-08-13 08:00
task 2: 2026-08-13 08:00 -> 2026-08-14 08:00
```

Each call does one job:

- [`Project.Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) with `data` loads and schedules the snapshot. It works on its own copy of `data`, so later changes to your object don't reach the project. The scheduled dates become the project's baseline.
- [`Project.Preview`](<https://docs.dhtmlx.com/planning-engine/net/api/project-preview.md>) runs the change on a copy of the baseline. The project doesn't change, so it's safe to call on every mouse move during a drag.
- [`Project.Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>) commits the change and marks the project as unsolved.
- [`Solve(null, ...)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) reschedules the current state. Task 1 is reported because it was edited, and task 2 moves to follow it.

Edits are [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md>). A [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/task-update.md>) sets only the fields you give it. Each field is a [`Patch<T>`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/patch.md>): a plain value such as `Duration = 3` sets it, [`Patch<double>.Null`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/patch.md#null>) clears it, and [`Patch<T>.Missing`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/patch.md#missing>) (the default) leaves it alone.

A `Project` isn't `IDisposable`, so there's nothing to release when you're done with it. It also isn't thread-safe: use one instance from one thread at a time.

### When an edit is rejected

[`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>) accepts what it can and reports the rest in the returned [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/apply-report.md>). It doesn't throw for a bad operation. To see this, remove a task that doesn't exist. Add this line before the `Apply` call:

```csharp
changes.Tasks.Remove.Add(99);
```

The report's [`Rejected`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/apply-report.md#rejected>) list now has an entry with this `Path` and `Message`:

```
tasks.remove[0] unknown task id
```

[`NeedsResync`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/apply-report.md#needsresync>) is now `true`. Your copy of the plan and the project's copy no longer match. The next [`Solve(null, ...)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) throws a [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) with `Code == ProjectError.NeedsResync` (message `needs-resync`). To fix it, send the full data again with `Solve(data, ...)`. That's why the example checks `NeedsResync` before it solves. [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/net/how-to/recover-from-rejected-changes.md>) covers this flow in detail.

## Next steps

- [Records, ids and dates](<https://docs.dhtmlx.com/planning-engine/net/records-ids-and-dates.md>): building records, [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>), [`Moment`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/moment.md>), [`Patch<T>`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/patch.md>) and read-only results.
- [Project data](<https://docs.dhtmlx.com/planning-engine/net/scheduling/project-data.md>): tasks, links, calendars, resources and assignments.
- [Scheduling a project](<https://docs.dhtmlx.com/planning-engine/net/scheduling/scheduling-a-project.md>): one-shot and stateful scheduling, options and subtrees.
- [Reading results](<https://docs.dhtmlx.com/planning-engine/net/scheduling/reading-results.md>): updates, conflicts, cycles and notes.
- [Working-time calendars](<https://docs.dhtmlx.com/planning-engine/net/scheduling/working-time-calendars.md>): hours, weekdays and date exceptions.
- [Editing a project](<https://docs.dhtmlx.com/planning-engine/net/live-projects/editing-a-project.md>): [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>), baselines and resync.
- [Previewing changes](<https://docs.dhtmlx.com/planning-engine/net/live-projects/previewing-changes.md>): what-if results without committing.
- [Leveling resources](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md>): remove resource overallocation.
- [Error handling](<https://docs.dhtmlx.com/planning-engine/net/error-handling.md>): [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>), [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) and what comes back as data instead.
- [Call from async code](<https://docs.dhtmlx.com/planning-engine/net/how-to/call-from-async-code.md>): run long calls off the request thread.
- [Share a project between threads](<https://docs.dhtmlx.com/planning-engine/net/how-to/share-a-project-between-threads.md>): clones and locking.
- [API reference](<https://docs.dhtmlx.com/planning-engine/net/api/overview.md>): every public type and method.
