Split assignment work#

Hand one task's work to several people, for example one developer for the first two days and another for the rest, without changing the task's dates.

Use the Offset and Duration properties of AssignmentData for this. Each assignment books only its own part of the task, called a slice. For how slices affect load, see Resources and assignments.

Steps#

  1. Book each person on their part of the task. Both properties are worked time in the task's DurationUnit, counted from the task start:

    • Offset: where the slice starts. null means the task start.
    • Duration: how much work the slice books. null means up to the task end.
    // Anna works the first 2 days of `build`.
    var anna = new AssignmentData { Id = "build:anna", Task = "build", Resource = "anna", Value = 1.0, Duration = 2.0 };
    // Ben starts after 2 days of work and finishes the task.
    var ben = new AssignmentData { Id = "build:ben", Task = "build", Resource = "ben", Value = 1.0, Offset = 2.0 };
    
  2. Pass the assignments to Project.Solve(data, …) as usual. Then check the booked load with Project.LoadProfile:

    var profile = project.LoadProfile(new ProfileQuery(from, to) { Basis = Basis.Day });
    
  3. To move the handover in a live project, send an AssignmentUpdate for each slice to Project.Apply, then call Solve(null, …):

    var changes = new SchedulingChanges { BaselineId = first.BaselineId };
    changes.Assignments.Update.Add(new AssignmentUpdate("build:anna") { Duration = 3.0 });
    changes.Assignments.Update.Add(new AssignmentUpdate("build:ben") { Offset = 3.0 });
    
  4. To book the whole task again, set the property to Patch<double>.Null. Offset = Patch<double>.Null goes back to the task start, and Duration = Patch<double>.Null goes up to the task end:

    var reset = new AssignmentUpdate("build:anna") { Duration = Patch<double>.Null };
    

The full example splits the 4-day build task between Anna and Ben, prints their daily load, then moves the handover one day later:

using Plen;

// `build`: 4 working days, Monday 2026-08-10 to Thursday.
var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks = [new TaskData("build") { StartDate = monday, Duration = 4.0 }],
    Resources = [new ResourceData { Id = "anna" }, new ResourceData { Id = "ben" }],
    Assignments =
    [
        // Anna works the first 2 days of `build`.
        new AssignmentData { Id = "build:anna", Task = "build", Resource = "anna", Value = 1.0, Duration = 2.0 },
        // Ben starts after 2 days of work and finishes the task.
        new AssignmentData { Id = "build:ben", Task = "build", Resource = "ben", Value = 1.0, Offset = 2.0 },
    ],
};

var project = new Project();
var first = project.Solve(data, new SolveOptions());
Console.WriteLine("handover after day 2");
PrintLoad(project);

// Move the handover one day later: Anna works 3 days, Ben the last one.
var changes = new SchedulingChanges { BaselineId = first.BaselineId };
changes.Assignments.Update.Add(new AssignmentUpdate("build:anna") { Duration = 3.0 });
changes.Assignments.Update.Add(new AssignmentUpdate("build:ben") { Offset = 3.0 });

var report = project.Apply(changes);
if (report.Rejected.Count > 0)
{
    throw new InvalidOperationException($"rejected: {string.Join("; ", report.Rejected)}");
}
var result = project.Solve(null, new SolveOptions());
Console.WriteLine($"handover after day 3, moved tasks: {result.Updates.Count}");
PrintLoad(project);

static void PrintLoad(Project project)
{
    // Daily load from Monday 2026-08-10 to Friday 2026-08-14.
    var from = Moment.FromYmdHm(2026, 8, 10, 0, 0) ?? throw new InvalidOperationException("invalid date");
    var to = Moment.FromYmdHm(2026, 8, 15, 0, 0) ?? throw new InvalidOperationException("invalid date");
    var profile = project.LoadProfile(new ProfileQuery(from, to) { Basis = Basis.Day });
    foreach (var resource in profile.Resources)
    {
        var loads = resource.Buckets.Select(b => b.Load);
        Console.WriteLine($"  {resource.Resource}: [{string.Join(", ", loads)}]");
    }
}

LoadProfile needs a solved project, so call Solve(null, …) after Apply and before you read the load again.

Things to check#

  • Slices never move tasks. A slice edit leaves every task date as it was, but it changes the BaselineId. A SchedulingChanges that still carries the old token throws ProjectException with Code == ProjectError.StaleBaseline.
  • The engine clips the slice to the task's work when it books load. A slice that runs past the task end books only up to the end. An Offset at or past the task's duration books nothing. If the task gets shorter later, the slice follows it.
  • Non-working time and leveling pauses don't count as worked time. A pause or a LevelingDelay moves the slice together with the work.
  • Two slices of the same resource on the same task add up where they overlap.
  • Strict validation rejects an Offset that's below 0 or not finite. It also rejects a Duration that's 0 or less, or not finite. Solve(data, …) throws InputException with Path set to assignments[i].offset or assignments[i].duration. In Apply, the same values show up as rejected operations in ApplyReport.Rejected, at paths like assignments.update[0]. See Recover from rejected changes.
  • With Validation.Repair, the engine clears an invalid value and adds a Note.Repaired, so the assignment books the whole task. See Accept messy input.
  • An assignment on an unscheduled task keeps its slice and books it again when the task is scheduled.

Result#

The example prints each person's load for Monday to Friday, before and after the handover moves:

handover after day 2
  anna: [1, 1, 0, 0, 0]
  ben: [0, 0, 1, 1, 0]
handover after day 3, moved tasks: 0
  anna: [1, 1, 1, 0, 0]
  ben: [0, 0, 0, 1, 0]

The load moves from Ben to Anna on Wednesday. build keeps its dates, so Solve(null, …) reports no moved tasks.