Skip to the content.

Task Scheduling Domain Builder - API Reference

Overview

The Task Scheduling domain builder defines and solves task scheduling problems with dependencies, resource constraints and deadlines. It provides:

  1. F# computation expression builders (scheduledTask { }, resource { }, scheduling { }) - dependencies are declared on the task that has them (after "TaskA")
  2. C# helpers (the Scheduling module: Scheduling.task, Scheduling.SchedulingBuilder) - method chaining for C#

Both produce the same SchedulingProblem<'TTask, 'TResource> record, which you solve with solveQuantumAsync (QUBO + QAOA on an IQuantumBackend). A classical scheduler that ignores resource capacities, ClassicalSolver.solve, is also public.


Design

open FSharp.Azure.Quantum
open FSharp.Azure.Quantum.TaskScheduling

// Simple case
let simple : ScheduledTask<unit> = scheduledTask {
    taskId "Task1"
    duration (hours 2.0)
}

// Complex case adds only what's needed
let complex : ScheduledTask<unit> = scheduledTask {
    taskId "Task2"
    duration (hours 1.5)
    after "Task1"
    requires "Worker" 2.0
    priority 10.0
    deadline (minutes 180.0)
}

// Task templates are ordinary functions
let createSafetyTask (taskName: string) (durationMins: float) : ScheduledTask<unit> =
    scheduledTask {
        taskId taskName
        duration (minutes durationMins)
        priority 10.0
    }

let electricalSafety = createSafetyTask "SafetyElectrical" 15.0
let mechanicalSafety = createSafetyTask "SafetyMechanical" 20.0

The builders are generic in the task payload type (ScheduledTask<'T>); tasks built with scheduledTask { } carry no payload (Value = None), so the examples fix it to unit with a type annotation.


Quick Start

F# Computation Expression

open System.Threading
open FSharp.Azure.Quantum
open FSharp.Azure.Quantum.TaskScheduling
open FSharp.Azure.Quantum.Backends
open FSharp.Azure.Quantum.Core.BackendAbstraction

// Define tasks with dependencies
let taskA : ScheduledTask<unit> = scheduledTask {
    taskId "TaskA"
    duration (hours 2.0)
}

let taskB : ScheduledTask<unit> = scheduledTask {
    taskId "TaskB"
    duration (minutes 30.0)
    after "TaskA"  // TaskB depends on TaskA
    deadline (minutes 180.0)
}

// Compose scheduling problem
let problem : SchedulingProblem<unit, unit> = scheduling {
    tasks [taskA; taskB]
    objective MinimizeMakespan
    timeHorizon (hours 3.0)  // Keep close to the expected makespan (see "How solveQuantumAsync works")
}

// Solve on the local simulator (2 tasks x 6 time slots = 12 qubits)
let backend = LocalBackend.LocalBackend() :> IQuantumBackend

task {
    match! solveQuantumAsync backend problem CancellationToken.None with
    | Ok solution ->
        printfn "Makespan: %.1f minutes" solution.Makespan.TotalMinutes
        exportGanttChart solution "schedule.txt"
    | Error err ->
        printfn "Failed: %s" err.Message
}

open FSharp.Azure.Quantum brings the builders, solveQuantumAsync, exportGanttChart and the time helpers into scope; open FSharp.Azure.Quantum.TaskScheduling brings the types (ScheduledTask, SchedulingProblem, Solution, the Objective cases, Dependency).

C#

using System;
using System.Threading;
using Microsoft.FSharp.Collections;
using Microsoft.FSharp.Control;
using Microsoft.FSharp.Core;
using FSharp.Azure.Quantum;
using FSharp.Azure.Quantum.Backends;
using FSharp.Azure.Quantum.TaskScheduling;

// Define tasks (id, payload, duration)
var taskA = Scheduling.task("TaskA", "TaskA-Value", TimeSpan.FromHours(2.0));
var taskB = Scheduling.taskWithRequirements(
    "TaskB",
    "TaskB-Value",
    TimeSpan.FromMinutes(30.0),
    ListModule.OfSeq(new[] { Tuple.Create("Worker", 1.0) }));

// Compose scheduling problem (dependencies are added separately).
// Create is itself generic, so its type arguments are given explicitly.
var problem = Scheduling.SchedulingBuilder<string, string>.Create<string, string>()
    .Tasks(ListModule.OfSeq(new[] { taskA, taskB }))
    .AddDependency(Types.Dependency.NewFinishToStart("TaskA", "TaskB", TimeSpan.Zero))
    .Objective(Scheduling.SchedulingObjective.MinimizeMakespan)
    .TimeHorizon(TimeSpan.FromHours(3.0))
    .Build();

// Solve
var backend = new LocalBackend.LocalBackend();
var result = await TaskSchedulingTypes.solveQuantumAsync(backend, problem, CancellationToken.None);

if (result.IsOk)
{
    Console.WriteLine($"Makespan: {result.ResultValue.Makespan.TotalMinutes} minutes");
}
else
{
    Console.WriteLine($"Failed: {result.ErrorValue.Message}");
}

Key Differences:


Builder Reference

1. scheduledTask Builder

Define individual tasks with duration, dependencies and constraints.

Available Operations:

Operation Parameters Description Example
taskId string ✅ Required - Unique task identifier taskId "TaskA"
duration TimeSpan ✅ Required - Task duration duration (hours 2.0)
after string Add single dependency (task ID) after "TaskA"
afterMultiple string list Add multiple dependencies afterMultiple ["A"; "B"]
requires string, float Add resource requirement (ID, quantity) requires "Worker" 2.0
priority float Tie-breaking priority (higher = more important) priority 10.0
deadline TimeSpan Latest completion time, as an offset from the schedule start deadline (minutes 180.0)
earliestStart TimeSpan Earliest allowed start time, as an offset earliestStart (minutes 60.0)

Missed deadlines are reported in Solution.DeadlineViolations; they do not make solveQuantumAsync fail.

earliestStart is a hard constraint for both solvers. priority only breaks ties: solveQuantumAsync picks, among sampled schedules that are equally good by the objective, the one with the smallest Σ priority × end time, so higher-priority tasks finish first. ClassicalSolver.solve starts every task at its own earliest feasible time, so priority does not change its result.

Examples:

// Simple task
let taskA : ScheduledTask<unit> = scheduledTask {
    taskId "TaskA"
    duration (minutes 30.0)
}

// Task with single dependency
let taskB : ScheduledTask<unit> = scheduledTask {
    taskId "TaskB"
    duration (hours 1.0)
    after "TaskA"  // TaskB starts after TaskA completes
}

// Task with multiple dependencies
let taskC : ScheduledTask<unit> = scheduledTask {
    taskId "TaskC"
    duration (hours 2.0)
    afterMultiple ["TaskA"; "TaskB"]  // TaskC starts after both complete
}

// Task with all options
let taskD : ScheduledTask<unit> = scheduledTask {
    taskId "TaskD"
    duration (hours 1.5)
    after "TaskC"
    requires "Worker" 2.0
    requires "Machine" 1.0
    priority 10.0
    deadline (minutes 300.0)
    earliestStart (minutes 60.0)
}

2. resource Builder

Define resources with capacity and cost.

Available Operations:

Operation Parameters Description Example
resourceId string ✅ Required - Unique resource identifier resourceId "Worker"
capacity float ✅ Required - Units available at any moment capacity 3.0
costPerUnit float Cost per unit per minute (default 0.0) costPerUnit 50.0
availableWindow float, float Availability window (start, end) in minutes from the schedule start, bounds inclusive (default: always available) availableWindow 0.0 480.0

Total cost is Σ costPerUnit × quantity × task duration in minutes, over every task’s requirements.

A task that requires a resource must run entirely inside one of that resource’s availability windows; both solvers enforce this. The availableWindow operation sets a single window (a second use replaces the first). For several windows, set the record field directly: { specialist with AvailableWindows = [ (0.0, 240.0); (480.0, 960.0) ] }.

Examples:

// Simple resource
let worker : Resource<unit> = resource {
    resourceId "Worker"
    capacity 3.0
}

// Resource with cost
let machine : Resource<unit> = resource {
    resourceId "Machine"
    capacity 2.0
    costPerUnit 100.0
}

// Resource available from minute 480 to minute 960 of the schedule;
// tasks that require it are placed inside that window
let specialist : Resource<unit> = resource {
    resourceId "Specialist"
    capacity 1.0
    costPerUnit 200.0
    availableWindow 480.0 960.0
}

Helper Function - crew:

crew id capacity costPerUnit creates a Resource<string> whose payload is its ID:

// Using builder
let crew1 : Resource<unit> = resource {
    resourceId "SafetyCrew"
    capacity 2.0
    costPerUnit 100.0
}

// Using helper (same capacity and cost, payload Some "SafetyCrew")
let crew2 = crew "SafetyCrew" 2.0 100.0

3. scheduling Builder

Compose complete scheduling problems from tasks and resources.

Available Operations:

Operation Parameters Description Example
tasks ScheduledTask list ✅ Required - Tasks to schedule; their after dependencies are collected here tasks [taskA; taskB]
resources Resource list Available resources (optional) resources [worker; machine]
objective Objective Optimization goal (default MinimizeMakespan) objective MinimizeCost
timeHorizon TimeSpan Scheduling window (default 1000 minutes) timeHorizon (hours 8.0)

Available Objectives:

Objective Description
MinimizeMakespan Finish all tasks as early as possible (default)
MinimizeCost Minimize total resource cost
MaximizeResourceUtilization Keep resources busy
MinimizeLateness Minimize total time past deadlines

In this model every task always uses exactly its required resources for its full duration, so total cost and total usage are the same for every feasible schedule. MinimizeCost and MaximizeResourceUtilization therefore pick the shortest feasible schedule, like MinimizeMakespan. MinimizeLateness picks the schedule with the least total lateness (makespan breaks ties).

Examples:

// Simple problem (no resources)
let problem1 : SchedulingProblem<unit, unit> = scheduling {
    tasks [taskA; taskB; taskC]
    objective MinimizeMakespan
}

// Problem with resources
let problem2 : SchedulingProblem<unit, unit> = scheduling {
    tasks [taskA; taskB; taskC]
    resources [worker; machine]
    objective MinimizeCost
}

// Problem with a time horizon
let problem3 : SchedulingProblem<unit, unit> = scheduling {
    tasks [taskA; taskB; taskC; taskD]
    resources [worker; machine; specialist]
    objective MinimizeMakespan
    timeHorizon (hours 10.0)
}

The builders are F# computation expressions; from C#, use the Scheduling module helpers shown in C# Interop.


Time Unit Helpers

Readable duration specifications. Each returns a System.TimeSpan:

Function Example Result
minutes minutes 30.0 TimeSpan.FromMinutes 30.0
hours hours 2.0 TimeSpan.FromHours 2.0 (120 minutes)
days days 1.0 TimeSpan.FromDays 1.0 (1440 minutes)

Examples:

let task1 : ScheduledTask<unit> = scheduledTask {
    taskId "Task1"
    duration (minutes 30.0)
}

let task2 : ScheduledTask<unit> = scheduledTask {
    taskId "Task2"
    duration (hours 2.0)
}

let task3 : ScheduledTask<unit> = scheduledTask {
    taskId "Task3"
    duration (days 1.0)
}

From C#, use TimeSpan.FromMinutes, TimeSpan.FromHours and TimeSpan.FromDays.


Functions

solveQuantumAsync

Solve the scheduling problem on a quantum backend.

Signature:

val solveQuantumAsync :
    backend:IQuantumBackend ->
    problem:SchedulingProblem<'TTask, 'TResource> ->
    cancellationToken:CancellationToken ->
    Task<QuantumResult<Solution>>

Note: This API returns a Task<_> and takes a CancellationToken, like the backend-level APIs. See Backend Switching for task { } patterns.

Parameters:

Returns (QuantumResult<Solution> is Result<Solution, QuantumError>):

Solution Fields:

Field Type Description
Assignments TaskAssignment list Task start/end times and resources
Makespan TimeSpan Total completion time (max end time)
TotalCost float Total resource usage cost
ResourceUtilization Map<string, float> Utilization per resource (0.0-1.0)
DeadlineViolations string list Task IDs that missed deadlines
IsValid bool True if no deadline violations
WasRepaired bool True when no measured sample was a feasible schedule as measured and the schedule comes from the one-hot repair decode (see below). Always false from ClassicalSolver.solve
Sampling SampleStatistics option From solveQuantumAsync: Shots, Qubits, Valid (final samples that are feasible schedules as measured) and Hits (final samples that are the returned schedule; 0 when it was repaired). None from ClassicalSolver.solve

TaskAssignment Fields:

Field Type Description
TaskId string Task identifier
StartTime TimeSpan Start time, as an offset from the schedule start
EndTime TimeSpan End time, as an offset from the schedule start
AssignedResources Map<string, float> Resources allocated (ID -> quantity)

Example:

task {
    match! solveQuantumAsync backend problem CancellationToken.None with
    | Ok solution ->
        printfn "Makespan: %.1f minutes" solution.Makespan.TotalMinutes
        printfn "Total Cost: $%.2f" solution.TotalCost
        printfn "Valid: %b" solution.IsValid

        printfn "\nTask Assignments:"
        solution.Assignments
        |> List.sortBy (fun a -> a.StartTime)
        |> List.iter (fun a ->
            printfn "  %s: [%.1f - %.1f]" a.TaskId a.StartTime.TotalMinutes a.EndTime.TotalMinutes)

        if not (List.isEmpty solution.DeadlineViolations) then
            printfn "\nDeadline Violations:"
            solution.DeadlineViolations |> List.iter (printfn "  - %s")

    | Error err ->
        printfn "Scheduling failed: %s" err.Message
}

Validation Checks (the same for both solvers):

Circular dependencies are not rejected up front: solveQuantumAsync then finds no feasible sample and returns an Error.

How solveQuantumAsync works

  1. Time slots. Time is split into a small grid of equal slots. The window is max timeHorizon (total task duration). The slot count starts from window ÷ shortest task duration, is capped so that tasks × slots stays near 18 (at most 10 slots, at least 2), and is never below the length of the longest dependency chain. Slot length = window ÷ slot count, and every task starts on a slot boundary.
  2. Allowed start slots. A (task, slot) pair is forbidden when the slot starts before the task’s earliestStart, or when a task started there would not fit inside an availability window of every resource it requires. If some task has no allowed slot on the grid, solveQuantumAsync returns a ValidationError on "AvailableWindows" naming the task, before running any circuit.
  3. QUBO. One binary variable per (task, slot) - so qubits = tasks × slots - with penalty terms for “start exactly once”, dependencies, resource capacity and forbidden start slots, plus the objective. Forbidden slots stay in the QUBO (with a penalty), so they still count towards the qubits. For MinimizeMakespan the QUBO objective is the sum of the tasks’ completion times, not the makespan (the latest completion time is not a quadratic function of the start bits). Two schedules with the same sum have the same energy even when their makespans differ: tasks of 20, 10 and 10 minutes with A → B and A, C sharing one machine have two lowest-energy schedules, of makespan 30 and 40. The makespan is applied in step 5.
  4. QAOA. The shared QAOA configuration (QaoaExecutionHelpers.QaoaSolverConfig). solveQuantumAsync uses QaoaExecutionHelpers.defaultConfig: 2 layers, angles optimised with Nelder-Mead, 1000 final shots. solveQuantumWithConfigAsync backend problem config cancellationToken takes your own (NumLayers, EnableOptimization, FinalShots, EnableConstraintRepair, …).
  5. Decode and check. A shot counts as valid only if every task has exactly one start slot set and the schedule keeps every dependency, resource capacity, earliestStart and resource window. The best valid shot by the objective (minimum makespan, or least lateness) is returned, with ties broken by priority; Sampling.Valid and Sampling.Hits count the valid shots and the shots equal to the returned schedule.
  6. Repair decode, reported. Only if no shot is valid, and EnableConstraintRepair is on (the default), the shots are read leniently: the bits of forbidden slots are cleared and a task with several set start bits takes its earliest one. Schedules read this way pass the same feasibility checks, and the solution has WasRepaired = true (with Sampling.Valid = 0 and Sampling.Hits = 0). With EnableConstraintRepair = false the result is an Error instead.

Consequences:

ClassicalSolver.solve

val ClassicalSolver.solve :
    problem:SchedulingProblem<'TTask, 'TResource> -> QuantumResult<Solution>

A greedy scheduler (in FSharp.Azure.Quantum.TaskScheduling): tasks are taken in dependency order and each starts as early as its predecessors, earliestStart and the availability windows of the resources it requires allow (in the earliest window that fits). If a task fits in no window, it returns a ValidationError on "AvailableWindows". It has no size limit and gives the exact earliest-start schedule when resource capacities don’t matter, but it ignores resource capacities: tasks competing for the same resource may overlap.

match ClassicalSolver.solve problem with
| Ok solution -> printfn "Makespan: %.1f minutes" solution.Makespan.TotalMinutes
| Error err -> printfn "Failed: %s" err.Message

exportGanttChart

Export the schedule as a Gantt chart in text format.

Signature:

val exportGanttChart : solution:Solution -> filePath:string -> unit

Parameters:

Output Format:

Example:

task {
    match! solveQuantumAsync backend problem CancellationToken.None with
    | Ok solution ->
        exportGanttChart solution "my-schedule.txt"
        printfn "Gantt chart saved!"
    | Error err ->
        printfn "Failed: %s" err.Message
}

Example Output File (the powerplant schedule from Example 4):

# Gantt Chart - Task Schedule

Makespan: 140.0 minutes
Total Cost: $0.00
Valid: true

Task Assignments:
----------------
SafetyElectr [   0.0 -   15.0] ███████████████
SafetyMechan [   0.0 -   20.0] ████████████████████
InitControl  [  15.0 -   40.0] █████████████████████████
InitCooling  [  20.0 -   50.0] ██████████████████████████████
StartPump1   [  50.0 -   60.0] ██████████
StartPump2   [  50.0 -   60.0] ██████████
StartTurbine [  60.0 -  105.0] █████████████████████████████████████████████
SyncGrid     [ 105.0 -  120.0] ███████████████
FullPower    [ 120.0 -  140.0] ████████████████████

Complete Examples

Example 1: Simple 3-Task Chain

F# Computation Expression

open FSharp.Azure.Quantum
open FSharp.Azure.Quantum.TaskScheduling

// Define tasks A → B → C
let taskA : ScheduledTask<unit> = scheduledTask {
    taskId "TaskA"
    duration (minutes 10.0)
}

let taskB : ScheduledTask<unit> = scheduledTask {
    taskId "TaskB"
    duration (minutes 20.0)
    after "TaskA"  // ✅ Dependency visible at definition
}

let taskC : ScheduledTask<unit> = scheduledTask {
    taskId "TaskC"
    duration (minutes 15.0)
    after "TaskB"  // ✅ Dependency visible at definition
}

let problem : SchedulingProblem<unit, unit> = scheduling {
    tasks [taskA; taskB; taskC]
    objective MinimizeMakespan
    timeHorizon (minutes 60.0)  // 6 slots of 10 minutes: 3 tasks x 6 slots = 18 qubits
}

let result =
    solveQuantumAsync backend problem CancellationToken.None
    |> Async.AwaitTask
    |> Async.RunSynchronously

// Best possible: makespan 45 minutes (sequential execution)

C#

using System;
using Microsoft.FSharp.Collections;
using FSharp.Azure.Quantum;
using FSharp.Azure.Quantum.TaskScheduling;

// Define tasks (dependencies added separately)
var taskA = Scheduling.task("TaskA", "A", TimeSpan.FromMinutes(10.0));
var taskB = Scheduling.task("TaskB", "B", TimeSpan.FromMinutes(20.0));
var taskC = Scheduling.task("TaskC", "C", TimeSpan.FromMinutes(15.0));

var problem = Scheduling.SchedulingBuilder<string, string>.Create<string, string>()
    .Tasks(ListModule.OfSeq(new[] { taskA, taskB, taskC }))
    .AddDependency(Types.Dependency.NewFinishToStart("TaskA", "TaskB", TimeSpan.Zero))  // TaskB after TaskA
    .AddDependency(Types.Dependency.NewFinishToStart("TaskB", "TaskC", TimeSpan.Zero))  // TaskC after TaskB
    .Objective(Scheduling.SchedulingObjective.MinimizeMakespan)
    .TimeHorizon(TimeSpan.FromMinutes(60.0))
    .Build();

// Solve with TaskSchedulingTypes.solveQuantumAsync as in the Quick Start

Example 2: Parallel Tasks with Resources

// Two tasks requiring the same resource
let jobA : ScheduledTask<unit> = scheduledTask {
    taskId "JobA"
    duration (hours 1.0)
    requires "Worker" 1.0
}

let jobB : ScheduledTask<unit> = scheduledTask {
    taskId "JobB"
    duration (hours 1.5)
    requires "Worker" 1.0
}

// Only 1 worker available
let oneWorker : Resource<unit> = resource {
    resourceId "Worker"
    capacity 1.0
    costPerUnit 50.0
}

let sharedProblem : SchedulingProblem<unit, unit> = scheduling {
    tasks [jobA; jobB]
    resources [oneWorker]
    objective MinimizeCost
    timeHorizon (hours 3.0)  // 3 slots of 1 hour: 2 tasks x 3 slots = 6 qubits
}

let sharedResult =
    solveQuantumAsync backend sharedProblem CancellationToken.None
    |> Async.AwaitTask
    |> Async.RunSynchronously
// Tasks are serialized because the worker has capacity 1

Example 3: Deadline-Constrained Scheduling

let prep : ScheduledTask<unit> = scheduledTask {
    taskId "Prep"
    duration (hours 1.0)
}

let delivery : ScheduledTask<unit> = scheduledTask {
    taskId "Delivery"
    duration (hours 2.0)
    after "Prep"
    deadline (minutes 150.0)  // Cannot be met: Prep + Delivery take 180 minutes
}

let deadlineProblem : SchedulingProblem<unit, unit> = scheduling {
    tasks [prep; delivery]
    objective MinimizeLateness
    timeHorizon (hours 3.0)
}

task {
    match! solveQuantumAsync backend deadlineProblem CancellationToken.None with
    | Ok solution ->
        if solution.IsValid then
            printfn "✅ All deadlines met!"
        else
            printfn "⚠️ Deadline violations: %A" solution.DeadlineViolations  // ["Delivery"]
    | Error err ->
        printfn "Failed: %s" err.Message
}

Example 3.5: Data-Driven Scheduling

Scenario: Load tasks from a database and build the schedule conditionally.

Custom operations such as after or deadline cannot be placed inside if/match in the builder. For conditional data, build the records directly - ScheduledTask and SchedulingProblem are ordinary F# records - and add the dependencies to the problem:

open System

// Database task model
type DbTask = {
    Name: string
    DurationMinutes: int
    Rank: int
    DependsOn: string option
    DeadlineMinutes: float option
    NeedsWorker: bool
}

// Rows as they might come from a database query
let dbTasks = [
    { Name = "Extract"; DurationMinutes = 30; Rank = 8; DependsOn = None; DeadlineMinutes = None; NeedsWorker = true }
    { Name = "Transform"; DurationMinutes = 45; Rank = 6; DependsOn = Some "Extract"; DeadlineMinutes = Some 120.0; NeedsWorker = false }
    { Name = "Archive"; DurationMinutes = 20; Rank = 2; DependsOn = None; DeadlineMinutes = None; NeedsWorker = false }
]

let environment = "Production"

let toTask (row: DbTask) : ScheduledTask<unit> = {
    Id = row.Name
    Value = None
    Duration = minutes (float row.DurationMinutes)
    EarliestStart = None
    Deadline = row.DeadlineMinutes |> Option.map minutes
    ResourceRequirements = if row.NeedsWorker then Map.ofList ["Worker", 1.0] else Map.empty
    Priority = float row.Rank
    Properties = Map.empty
}

// Filter high-priority rows; a dependency on a filtered-out task would fail validation
let selected = dbTasks |> List.filter (fun t -> t.Rank >= 5)

let dependencies =
    selected
    |> List.choose (fun t -> t.DependsOn |> Option.map (fun pred -> FinishToStart(pred, t.Name, TimeSpan.Zero)))

let dbProblem : SchedulingProblem<unit, unit> =
    let baseProblem = scheduling {
        tasks (selected |> List.map toTask)
        objective (if environment = "Production" then MinimizeMakespan else MinimizeCost)
        timeHorizon (hours 2.0)
    }
    { baseProblem with Dependencies = dependencies }

From C#, the same is done with a loop that fills a task list and calls .AddDependency(...) for each dependency before .Build().


Example 4: Powerplant Startup

A nine-task startup sequence with a six-task critical path.

// Phase 1: Safety checks (parallel)
let safetyElectrical : ScheduledTask<unit> = scheduledTask {
    taskId "SafetyElectrical"
    duration (minutes 15.0)
    priority 10.0
}

let safetyMechanical : ScheduledTask<unit> = scheduledTask {
    taskId "SafetyMechanical"
    duration (minutes 20.0)
    priority 10.0
}

// Phase 2: System initialization
let initCooling : ScheduledTask<unit> = scheduledTask {
    taskId "InitCooling"
    duration (minutes 30.0)
    afterMultiple ["SafetyElectrical"; "SafetyMechanical"]
}

let initControl : ScheduledTask<unit> = scheduledTask {
    taskId "InitControl"
    duration (minutes 25.0)
    after "SafetyElectrical"
}

// Phase 3: Component startup
let startPump1 : ScheduledTask<unit> = scheduledTask {
    taskId "StartPump1"
    duration (minutes 10.0)
    after "InitCooling"
}

let startPump2 : ScheduledTask<unit> = scheduledTask {
    taskId "StartPump2"
    duration (minutes 10.0)
    after "InitCooling"
}

let startTurbine : ScheduledTask<unit> = scheduledTask {
    taskId "StartTurbine"
    duration (minutes 45.0)
    afterMultiple ["StartPump1"; "StartPump2"; "InitControl"]
}

// Phase 4: Power generation
let syncGrid : ScheduledTask<unit> = scheduledTask {
    taskId "SyncGrid"
    duration (minutes 15.0)
    after "StartTurbine"
}

let fullPower : ScheduledTask<unit> = scheduledTask {
    taskId "FullPower"
    duration (minutes 20.0)
    after "SyncGrid"
    deadline (minutes 180.0)  // Must reach full power within 180 minutes
}

let startupProblem : SchedulingProblem<unit, unit> = scheduling {
    tasks [
        safetyElectrical; safetyMechanical
        initCooling; initControl
        startPump1; startPump2; startTurbine
        syncGrid; fullPower
    ]
    objective MinimizeMakespan
    timeHorizon (minutes 180.0)
}

// No shared resources, so the capacity-blind classical scheduler is exact here
match ClassicalSolver.solve startupProblem with
| Ok solution ->
    printfn "Powerplant Startup Schedule"
    printfn "Makespan: %.1f minutes" solution.Makespan.TotalMinutes
    exportGanttChart solution "powerplant-schedule.txt"
| Error err ->
    printfn "Failed: %s" err.Message

Result:

solveQuantumAsync is not an option for this problem on the local simulator: the six-task chain needs at least 6 slots, so the grid is 9 tasks × 6 slots = 54 qubits, and LocalBackend returns an Error saying so.


C# Interop

The computation expression builders are F#-only. C# uses the Scheduling module in FSharp.Azure.Quantum:

Member Description
Scheduling.task(id, value, duration) Task with a payload and a TimeSpan duration
Scheduling.taskWithRequirements(id, value, duration, requirements) Same, plus an F# list of (resourceId, quantity) tuples
Scheduling.SchedulingBuilder<TTask, TResource>.Create<TTask, TResource>() Fluent builder: .Tasks, .Resources, .AddDependency, .Objective, .TimeHorizon, .Build(). Create has its own type parameters, which C# cannot infer, so pass them explicitly
Scheduling.SchedulingObjective.MinimizeMakespan (etc.) The Objective values
Types.Dependency.NewFinishToStart(pred, succ, lag) A finish-to-start dependency with a TimeSpan lag
TaskSchedulingTypes.solveQuantumAsync(backend, problem, cancellationToken) The solver; returns Task<FSharpResult<Solution, QuantumError>>
TaskSchedulingTypes.exportGanttChart(solution, path) Gantt chart export

Awaiting the Result

using System.Threading;
using System.Threading.Tasks;
using Microsoft.FSharp.Core;

var result = await TaskSchedulingTypes.solveQuantumAsync(backend, problem, CancellationToken.None);
if (result.IsOk)
{
    foreach (var assignment in result.ResultValue.Assignments)
    {
        Console.WriteLine($"{assignment.TaskId}: [{assignment.StartTime} - {assignment.EndTime}]");
    }
}

Best Practices

1. Use Time Unit Helpers

✅ Good:

duration (hours 2.0)
duration (minutes 30.0)

Durations are TimeSpan values, so a bare number such as duration 120.0 does not compile - the unit is always explicit.

2. Co-locate Dependencies

✅ Good:

let taskB : ScheduledTask<unit> = scheduledTask {
    taskId "TaskB"
    duration (hours 1.0)
    after "TaskA"  // Dependency visible at definition
}

3. Use Meaningful Task IDs

Prefer "InitCoolingSystem", "StartPump1", "SafetyElectricalCheck" over "Task1", "T2", "X" - the IDs appear in results, errors and Gantt charts (cut to 12 characters there).

4. Set timeHorizon for solveQuantumAsync

Pick a horizon near the expected makespan (the total duration of the critical path, or of all tasks if they share one resource). The slot grid is built from it; see How solveQuantumAsync works.

5. Use Deadlines for Time-Critical Tasks

let criticalTask : ScheduledTask<unit> = scheduledTask {
    taskId "EmergencyShutdown"
    duration (minutes 10.0)
    deadline (minutes 60.0)  // Must complete within 60 minutes
}

Troubleshooting

Issue: “Duplicate task IDs found”

Cause: Two or more tasks have the same ID.

Solution: Ensure all task IDs are unique:

// ❌ Bad
let dup1 : ScheduledTask<unit> = scheduledTask { taskId "Task"; duration (minutes 5.0) }
let dup2 : ScheduledTask<unit> = scheduledTask { taskId "Task"; duration (minutes 5.0) }  // Duplicate!

// ✅ Good
let uniqueA : ScheduledTask<unit> = scheduledTask { taskId "TaskA"; duration (minutes 5.0) }
let uniqueB : ScheduledTask<unit> = scheduledTask { taskId "TaskB"; duration (minutes 5.0) }

Issue: “Invalid task dependencies reference non-existent tasks”

Cause: A task depends on a task ID that is not in the problem.

Solution: Check all after and afterMultiple references against the tasks list:

// ❌ Bad: TaskX is not part of the problem
let orphan : ScheduledTask<unit> = scheduledTask {
    taskId "TaskB"
    duration (minutes 5.0)
    after "TaskX"
}

Issue: “the schedule needs N qubits … and (backend) can run M”

Cause: tasks × time slots exceeds what the backend can run (see How solveQuantumAsync works).

Solution: Split the problem, shorten long dependency chains, use a larger backend, or - when resources don’t matter - use ClassicalSolver.solve.

Issue: ValidationError on “AvailableWindows”

Cause: A task cannot start anywhere that satisfies its earliestStart and fits inside an availability window of every resource it requires. With ClassicalSolver.solve no window is long enough after the task becomes ready. With solveQuantumAsync no slot boundary of the grid qualifies, for example because earliestStart lies beyond the scheduling window or a window is shorter than the slot spacing allows.

Solution: Widen or add windows, shorten the task, or with solveQuantumAsync set timeHorizon so that the grid reaches the window and has a slot boundary inside it.

Issue: “No valid solutions found from quantum measurements”

Cause: None of the final samples is a feasible schedule as measured (exactly one start slot per task, every dependency, resource limit, earliest start and availability window kept), and the repair decode found none either, or is switched off. This is more likely with tight horizons and many constraints.

Solution: Run again, pass a configuration with more FinalShots or NumLayers to solveQuantumWithConfigAsync, give the problem a little more room in timeHorizon, or reduce the problem size.

Issue: Tasks overlap on a shared resource

Cause: ClassicalSolver.solve ignores resource capacities.

Solution: Use solveQuantumAsync, which rejects samples that overload a resource, or add explicit dependencies to force serialization:

let first : ScheduledTask<unit> = scheduledTask {
    taskId "First"
    duration (hours 1.0)
}

let second : ScheduledTask<unit> = scheduledTask {
    taskId "Second"
    duration (hours 1.0)
    after "First"  // Force serialization
}

Limitations


See Also


Support & Feedback


License: Unlicense (Public Domain)