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


# SuggestResult

A staffing proposal scored against the project's accepted state. [`Project.SuggestAssignments`](<https://docs.dhtmlx.com/planning-engine/net/api/project-suggest-assignments.md>) returns it; there's no other way to get one. The project doesn't change until you pass `Changes` to [`Project.Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>).

## Declaration

```csharp
namespace Plen;

public sealed class SuggestResult : IDeepCloneable<SuggestResult>
{
    public required string BaselineId { get; set; }
    public required SchedulingChanges Changes { get; set; }
    public List<SuggestStep> Steps { get; set; } = [];
    public List<Id> Unfilled { get; set; } = [];
    public List<Id> Invalid { get; set; } = [];
    public AssignTotal Baseline { get; set; }
    public required SuggestProposal Proposed { get; set; }

    public SuggestResult DeepClone();
}
```

`SuggestResult` has no value equality: `Equals` compares references. `ToString()` lists the fields, for debugging.

## Properties

| Property | Type | Description |
|---|---|---|
| `BaselineId` | `string` | Token of the accepted state the proposal was scored against. Always set. |
| `Changes` | [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md>) | The net proposal against the accepted state. Pass it to [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>) as is. |
| `Steps` | `List<`[`SuggestStep`](<https://docs.dhtmlx.com/planning-engine/net/api/core/suggest/suggest-result.md#suggeststep>)`>` | Decisions that changed the working state, in the order they were made. |
| `Unfilled` | `List<`[`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>)`>` | Ids of targeted empty slots that stayed empty, in visit order. |
| `Invalid` | `List<`[`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>)`>` | Ids of slots whose final resource isn't in their `Allowed` list, in visit order. |
| `Baseline` | [`AssignTotal`](<https://docs.dhtmlx.com/planning-engine/net/api/core/suggest/suggest-result.md#assigntotal>) | Cost and resource count of the reference state, on the scoring stage. |
| `Proposed` | [`SuggestProposal`](<https://docs.dhtmlx.com/planning-engine/net/api/core/suggest/suggest-result.md#suggestproposal>) | Cost, resource count and metrics of the final working state. |

### BaselineId

```csharp
public required string BaselineId { get; set; }
```

The project's accepted baseline token at the time of the call. `Changes.BaselineId` holds the same value, so `Apply` throws [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) [`StaleBaseline`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md#projecterror>) if the project changed in between. With [`SuggestOptions.ClearFirst`](<https://docs.dhtmlx.com/planning-engine/net/api/core/suggest/suggest-options.md>) it's still the accepted state's token.

### Changes

```csharp
public required SchedulingChanges Changes { get; set; }
```

The difference between the accepted state and the final working state. It holds:

- one `Assignments.Add` entry per filled slot: `Id` = the slot id, the slot's `Task`, the chosen `Resource` and the slot's `Value`. `Offset` and `Duration` are `null`, so the assignment covers the whole task
- one `Assignments.Update` entry per repaired or rebalanced assignment, setting only `Resource`. None when the slot ended on its original resource
- with `ClearFirst`, one `Tasks.Update` per cleared task, setting `LevelingDelay` to `0` and `Pauses` to an empty list

`Changes` never removes an assignment and carries no leveling delays. With no steps and nothing cleared, every bucket is empty.

### Steps

```csharp
public List<SuggestStep> Steps { get; set; } = [];
```

Only decisions that changed the working state. A slot filled in pass 0 and moved in a later pass has two steps but one add in `Changes`. Tell the kind of a step from its fields:

| `FromResource` | `Pass` | Step |
|---|---|---|
| `null` | `0` | Fill |
| set | `0` | Repair |
| set | `1` or more | Rebalance |

### Unfilled

```csharp
public List<Id> Unfilled { get; set; } = [];
```

Empty slots that pass 0 targeted but couldn't fill, because no candidate could be scored. Empty slots outside `SuggestOptions.Targets` aren't listed.

### Invalid

```csharp
public List<Id> Invalid { get; set; } = [];
```

Every slot held by a resource outside its `Allowed` list at the end of the call, whatever `Targets` is. Slots bound to a `SuggestOptions.Fixed` assignment are included. With `Targets` set to an empty list, this is the only output, so the call works as a check.

### Baseline

```csharp
public AssignTotal Baseline { get; set; }
```

The reference state scored on the same stage as the steps: the accepted state, or its cleared copy with `ClearFirst`. With `SuggestOptions.Level`, it's that state leveled as is. The first step's `Cost.Before` equals `Baseline.Cost`.

### Proposed

```csharp
public required SuggestProposal Proposed { get; set; }
```

The final working state, measured against the reference state. Compare `Proposed.Cost` with `Baseline.Cost` to see what the proposal gains.

## Methods

### DeepClone

```csharp
public SuggestResult DeepClone();
```

Returns a copy that shares no list or nested object with this one.

## SuggestStep

One decision that changed the working state.

```csharp
namespace Plen;

public sealed class SuggestStep : IDeepCloneable<SuggestStep>, IEquatable<SuggestStep>
{
    public required Id Slot { get; set; }
    public required Id Task { get; set; }
    public uint Pass { get; set; }
    public Id? FromResource { get; set; }
    public required Id ToResource { get; set; }
    public StepCost Cost { get; set; }

    public SuggestStep DeepClone();
    public bool Equals(SuggestStep? other);
}
```

| Property | Type | Description |
|---|---|---|
| `Slot` | [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>) | The slot id. |
| `Task` | [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>) | The slot's task. |
| `Pass` | `uint` | `0` for a fill or repair; `1` and up for the rebalance pass that made the move. |
| `FromResource` | [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>)`?` | The resource before the step. `null` when the step filled an empty slot. |
| `ToResource` | [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/id.md>) | The resource after the step. |
| `Cost` | [`StepCost`](<https://docs.dhtmlx.com/planning-engine/net/api/core/suggest/suggest-result.md#stepcost>) | State cost before and after the step. |

Equality is by value over all properties.

## StepCost

The cost of the working state before and after one step.

```csharp
namespace Plen;

public record struct StepCost(double Before, double After);
```

| Property | Type | Description |
|---|---|---|
| `Before` | `double` | Cost before the step: the previous step's `After`, or `Baseline.Cost` for the first step. |
| `After` | `double` | Cost after the step. |

Both values include the new-resource penalty and are measured against the reference state on the scoring stage. Equality compares both values with `==`.

## AssignTotal

The cost and resource count of one assignment state. `SuggestResult.Baseline` holds one.

```csharp
namespace Plen;

public record struct AssignTotal(double Cost, uint Resources);
```

| Property | Type | Description |
|---|---|---|
| `Cost` | `double` | Cost of the state on the scoring stage. |
| `Resources` | `uint` | Resources of the counted pool with at least one active assignment. The counted pool is every resource in any slot's `Allowed`, plus the current resources of filled slots. |

Equality compares both values with `==`.

## SuggestProposal

The final working state: its cost, resource count and metrics against the reference state.

```csharp
namespace Plen;

public sealed class SuggestProposal : IDeepCloneable<SuggestProposal>, IEquatable<SuggestProposal>
{
    public double Cost { get; set; }
    public uint Resources { get; set; }
    public required Metrics Scheduled { get; set; }
    public LeveledMetrics? Leveled { get; set; }

    public SuggestProposal DeepClone();
    public bool Equals(SuggestProposal? other);
}
```

| Property | Type | Description |
|---|---|---|
| `Cost` | `double` | Cost of the final state, new-resource penalty included. Equals the last step's `Cost.After`. |
| `Resources` | `uint` | Resources of the counted pool with at least one active assignment. |
| `Scheduled` | [`Metrics`](<https://docs.dhtmlx.com/planning-engine/net/api/core/evaluate/metrics.md>) | Metrics of the scheduled stage. |
| `Leveled` | [`LeveledMetrics`](<https://docs.dhtmlx.com/planning-engine/net/api/core/evaluate/leveled-metrics.md>)`?` | Metrics of the leveled stage. Set only when [`SuggestOptions.Level`](<https://docs.dhtmlx.com/planning-engine/net/api/core/suggest/suggest-options.md#level>) is set; `null` otherwise and when the scheduled stage is cyclic. |

The metrics come from the last step's scoring run. With no steps, they describe the reference state on the scoring stage: scheduled deltas are zero, leveled ones may not be.

Equality is by value: `Cost` and `Resources` by `==`, `Scheduled` and `Leveled` by their own equality.

## Example

Fill an open position on task `C`, read the proposal, then commit it:

```csharp
using Plen;

var start = Moment.FromYmdHm(2026, 8, 10, 0, 0) ?? throw new InvalidOperationException("invalid date");
var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = start, Duration = 2.0 },
        new TaskData("C") { StartDate = start, Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }, new ResourceData { Id = "dev2" }],
    Assignments = [new AssignmentData { Id = "aA", Task = "A", Resource = "dev1", Value = 1.0 }],
};
data.Config.UseCalendars = false;

var project = new Project();
project.Solve(data, new SolveOptions());

List<Slot> slots = [new Slot("sC", "C", ["dev1", "dev2"])];
var result = project.SuggestAssignments(slots, new SuggestOptions());

foreach (var step in result.Steps)
{
    var kind = step.FromResource is null ? "fill" : step.Pass == 0 ? "repair" : "rebalance";
    Console.WriteLine($"{kind} {step.Slot}: {step.FromResource} -> {step.ToResource}, " +
                      $"cost {step.Cost.Before} -> {step.Cost.After}");
}
Console.WriteLine($"unfilled: [{string.Join(", ", result.Unfilled)}], invalid: [{string.Join(", ", result.Invalid)}]");
Console.WriteLine($"cost {result.Baseline.Cost} -> {result.Proposed.Cost}, " +
                  $"people {result.Baseline.Resources} -> {result.Proposed.Resources}");

var report = project.Apply(result.Changes);
if (report.Rejected.Count == 0)
    project.Solve(null, new SolveOptions());
```

## Related

- [`Project.SuggestAssignments`](<https://docs.dhtmlx.com/planning-engine/net/api/project-suggest-assignments.md>) - the call that returns it
- [`SuggestOptions`](<https://docs.dhtmlx.com/planning-engine/net/api/core/suggest/suggest-options.md>) - the options of the call
- [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md>) - the type of `Changes`
- [`Project.Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>) - commits `Changes`
- [Suggesting assignments](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/suggesting-assignments.md>) - how picks are made and how to read the proposal
