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:
- F# computation expression builders (
scheduledTask { },resource { },scheduling { }) - dependencies are declared on the task that has them (after "TaskA") - C# helpers (the
Schedulingmodule: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
- Business terms, not quantum terms: tasks, dependencies, resources and deadlines. The QUBO encoding and the QAOA circuit are internal.
- Dependencies at the definition point:
afterandafterMultiplesit inside the task that depends on something, so reading a task shows what it waits for. - Progressive disclosure: a task needs only
taskIdandduration; addafter,requires,deadline,priorityorearliestStartwhen needed. - Real time units: durations, deadlines and horizons are
TimeSpanvalues, built withminutes,hoursanddays.
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:
- F# builder: dependencies declared on the task (
after "TaskA"); tasks have no payload - C# helpers: dependencies added separately (
.AddDependency(...)); tasks carry a payload value - Both use
TimeSpanfor durations, deadlines and the horizon - The C# code references FSharp.Core (
ListModule,FSharpAsync,FSharpOption), which comes with the package
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 aCancellationToken, like the backend-level APIs. See Backend Switching fortask { }patterns.
Parameters:
backend- AnyIQuantumBackend, e.g.LocalBackend.LocalBackend() :> IQuantumBackendproblem- Scheduling problem defined withscheduling { ... }
Returns (QuantumResult<Solution> is Result<Solution, QuantumError>):
Ok solution- The best feasible schedule foundError err- Validation failure, a problem too large for the backend, or no feasible schedule among the measured samples
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):
- All tasks have non-empty, unique IDs
- All dependencies reference existing tasks
Circular dependencies are not rejected up front: solveQuantumAsync then finds no feasible sample and returns an Error.
How solveQuantumAsync works
- 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. - 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,solveQuantumAsyncreturns aValidationErroron"AvailableWindows"naming the task, before running any circuit. - 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
MinimizeMakespanthe 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. - QAOA. The shared QAOA configuration (
QaoaExecutionHelpers.QaoaSolverConfig).solveQuantumAsyncusesQaoaExecutionHelpers.defaultConfig: 2 layers, angles optimised with Nelder-Mead, 1000 final shots.solveQuantumWithConfigAsync backend problem config cancellationTokentakes your own (NumLayers,EnableOptimization,FinalShots,EnableConstraintRepair, …). - 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,
earliestStartand resource window. The best valid shot by the objective (minimum makespan, or least lateness) is returned, with ties broken by priority;Sampling.ValidandSampling.Hitscount the valid shots and the shots equal to the returned schedule. - Repair decode, reported. Only if no shot is valid, and
EnableConstraintRepairis 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 hasWasRepaired = true(withSampling.Valid = 0andSampling.Hits = 0). WithEnableConstraintRepair = falsethe result is anErrorinstead.
Consequences:
- Set
timeHorizonclose to the expected makespan. With the default 1000-minute window, a short problem gets slots of well over an hour, so tasks can only start at those coarse boundaries and the makespan is padded accordingly. - Size limit. The backend must be able to run tasks × slots qubits. For
LocalBackendthe limit is the smaller of its memory capacity (StateVector.maxQubits, derived from available memory, at most 30) and its wall-clock budget (20 qubits by default, raised with theFSAQ_MAX_CIRCUIT_QUBITSenvironment variable). A larger problem returns anErrornaming the qubits it needs; long dependency chains raise the slot count and hit this first. - Run time grows with the qubits. The default configuration optimises the angles by simulating the circuit a few hundred times. On
LocalBackenda default run takes about 5 seconds at 12 qubits, over a minute at 16 and several minutes at 18.solveQuantumWithConfigAsyncwithQaoaExecutionHelpers.fastConfig(one layer, grid search) or a smallerMaxOptimizationIterationsis quicker. - Results are sampled. A run can come back with
Error "No valid solutions found", or with a schedule that is feasible but not optimal. CheckWasRepairedandSamplingto see how the returned schedule stood among the samples.
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:
solution- Scheduling solution fromsolveQuantumAsyncorClassicalSolver.solvefilePath- Output file path (e.g., “schedule.txt”); the file is overwritten
Output Format:
- Header with makespan (minutes), total cost, and validity
- Task assignments sorted by start time, names cut to 12 characters
- Bars of █ characters, one per minute of duration
- Deadline violations list (if any)
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:
- Critical path: SafetyMechanical → InitCooling → StartPump1/StartPump2 → StartTurbine → SyncGrid → FullPower
- Makespan: 140 minutes; the 180-minute deadline is met
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
solveQuantumAsynchandles small problems only (tasks × slots within the backend’s qubit budget; about 20 qubits onLocalBackendby default).solveQuantumAsyncstart times fall on slot boundaries, so anearliestStartor window opening between two boundaries moves the task to the next boundary; a coarse grid can leave a short window with no allowed slot.ClassicalSolver.solveignores resource capacities.priorityonly breaks ties between equally good schedules.- Circular dependencies are not detected up front.
See Also
- Getting Started Guide - Installation and first steps
- API Reference - Complete API documentation
- Graph Coloring API - Similar computation expression pattern
- Computation Expressions Reference - All custom operations
- Working Examples - Task scheduling examples on GitHub
Support & Feedback
- GitHub: Thorium/FSharp.Azure.Quantum
- Issues: Report bugs or request features
- Documentation: See
docs/folder for more examples
License: Unlicense (Public Domain)