PowerSolver 2.0 — Concepts Guide
Read this once. Everything else in the docs assumes you understand the vocabulary here.
1. The five core nouns
| Term | What it is |
|---|---|
| Collection | A named pool of items the solver knows about (e.g. resources, tasks). Each item has an id and a properties bag. |
| Variable | An assignment relation. One of two types: basic (one-to-one) or list (one host owns an ordered list). |
| Entity | An item the solver makes decisions about — gets placed in a list, or gets a value assigned to it. |
| Host | For a list variable: the collection that OWNS the lists. Each host item carries its own ordered list of entities. |
| Constraint | A scoring rule. Each is hard (must-satisfy) or soft (optimise). |
Two more nouns that the solver fills in automatically:
| Term | What it is |
|---|---|
| Built-in shadow | A reactive read-only value the solver maintains on every entity in a list: previous, next, host, index. |
| Derived shadow | A computed value declared by an expression — e.g. startTime = previous?.endTime ?? host.startTime. The solver recomputes it whenever its sources change. |
2. Variable types — only two in 2.0
basic — one entity, one value
Each entity in collection X is assigned exactly one value from collection Y. The relation lives ON X and points TO Y.
Use it for: room → time-slot pairings, employee → shift one-shot picks, anything where the entity needs one decision and there's no ordering involved.
{
"id": "assignment",
"type": "basic",
"on": "tasks",
"valueRange": "employees"
}
list — host owns an ordered list
Each item in the host collection carries an ordered list of items from the entity collection. The list itself is the variable; the solver decides which entities go into which host's list and in what order.
Use it for: scheduling consecutive tasks on a machine, routing visits along a vehicle, sequencing maintenance jobs for a crew.
{
"id": "schedule",
"type": "list",
"on": "resources",
"valueRange": "tasks"
}
The 1.x chained type is gone. Anything chained variables used to do (chain-anchored routes, time-window propagation, cumulative-load tracking) is now expressed as a list variable plus one or more derived shadows. See §4.
3. Built-in shadows — automatic on every list element
When you declare a list variable, you can opt in to any of these four built-in shadows by adding an entry under top-level shadows[]. Each one is updated by the solver for free — no expression, no computation cost from your side.
| Shadow kind | What it returns | Typical use |
|---|---|---|
previousElement | The element BEFORE this one in its host's list (or null if first). | Computing back-to-back start times: previous?.endTime. |
nextElement | The element AFTER this one in its host's list (or null if last). | Look-ahead constraints. |
inverseRelation | The host that owns the list containing this element. | Cross-referencing host-level facts: host.startTime. |
index | The 0-based position in the host's list. | Order-sensitive penalties. |
Declaration:
"shadows": [
{"on": "tasks", "name": "previous", "kind": "previousElement", "source": "schedule"},
{"on": "tasks", "name": "host", "kind": "inverseRelation", "source": "schedule"},
{"on": "tasks", "name": "next", "kind": "nextElement", "source": "schedule"},
{"on": "tasks", "name": "position", "kind": "index", "source": "schedule"}
]
source is the id of the list variable these shadows track.
4. Derived shadows — the §5bis innovation
A derived shadow is a supplier expression that computes a value per entity from any combination of built-in shadows, host properties, and the entity's own properties. The solver re-runs the supplier whenever a source changes.
Canonical pair — back-to-back time scheduling
"derivedShadows": [
{
"on": "tasks",
"name": "startTime",
"sources": ["previous.endTime", "host.startTime"],
"expression": "previous?.endTime ?? host.startTime"
},
{
"on": "tasks",
"name": "endTime",
"sources": ["startTime", "this.duration"],
"expression": "startTime + this.duration"
}
]
startTimefor the FIRST task on a host = the host's ownstartTime(from the host's properties bag). For any subsequent task = the previous task'sendTime.endTime=startTime+ the task's owndurationproperty (an ISO-8601 string like"PT45M").
This pair is the §5bis replacement for the entire 1.x TimeGrain machinery. No fixed-grid time slots, no grain size, no anchor configuration.
Expression DSL
The expression mini-language is deliberately small so it stays auditable in the solver hot loop:
| Construct | Meaning |
|---|---|
propertyName | A reference to a known shadow / variable name on this entity. |
path.to.property | Dotted access — each segment must appear in the sources array. |
a?.b | Null-safe access — if a is null, the whole expression evaluates to null instead of throwing. |
a ?? b | Null-coalesce — returns a if non-null else b. |
a + b, a - b, a * b, a / b | Arithmetic. Operates per Java semantics on Number, Duration, LocalDateTime. |
this.propertyName | Explicit access to a problem-fact property on this entity (e.g. this.duration). Distinguishes from shadows of the same name. |
(expr) | Parentheses for grouping. |
No function calls, no loops, no assignments. If you need something the DSL doesn't cover, add a built-in rule in powersolver-api.
Sources are mandatory
Every property path the expression reads MUST appear in the sources array. The solver uses that array to decide when to re-run the supplier. Missing sources surface as validation errors at problem-load time.
At least one source must be variable-typed
A derived shadow whose only source is a static entity property — e.g. an expression like 5 - this.priority whose only path is this.priority — is rejected at problem-load time:
derivedShadow '<name>' has no variable-typed sources to declare to Timefold.
Add a path whose final segment is a variable (a built-in shadow like
'previous'/'host'/'next'/'index', or another derived shadow).
The solver needs at least one variable (a built-in shadow or another derived shadow) in the sources so it knows when to recompute. If your formula only needs static properties, either fold it inline into a shadow that already has a variable source (e.g. multiply by position rather than introducing a separate shadow), or use the static value directly in the constraint.
See it in action
For a step-by-step worked example — a real placement, the trace of every startTime / endTime value the derived shadows produce, and how the picture changes when the order changes — see Understanding "Consecutive on a host" in the Wizard Guide.
5. The collections map
In 2.0 the entities-being-assigned and the value-ranges-they-are-assigned-from live in a single top-level object:
"collections": {
"resources": [
{"id": "R1", "properties": {"name": "Bay-1", "startTime": "2026-06-01T09:00:00"}}
],
"tasks": [
{"id": "T1", "properties": {"name": "Cut", "priority": 1, "duration": "PT30M"}},
{"id": "T2", "properties": {"name": "Bevel", "priority": 2, "duration": "PT45M"}}
]
}
There is no top-level entities[] or valueRanges{} in 2.0 — that 1.x split is gone. Variables, shadows, and derived shadows reference these collections by name.
5b. Three kinds of lists — and what "fixed" means
A plan holds three kinds of lists in collections:
| Kind | Role | Job-shop example | School example |
|---|---|---|---|
| Items to plan | What gets planned (the on of the decisions) | operations | lessons |
| Options of a decision | What the planner chooses from (a decision's valueRange) | machines, time slots | rooms, time slots |
| Reference list | Data the rules look up; nothing is chosen from it | operators | teachers |
Each item names its entry of a reference list in one of its own columns (a lesson's teacher is MA-2); the list says who MA-2 is (subjects taught, service hours, half-days away). Three rules can read it, by naming that column in resource_property instead of a decision in variable_field:
| Rule | Checks against the reference list |
|---|---|
resource_unavailable | The item is not placed at a time its person or thing is away |
skill_match | Its person or thing has what the item needs (e.g. a certification) |
capacity | Its person or thing is not given more than its limit (hours, count, budget) |
When skill_match or capacity on a reference list is broken by the data alone, Can this work? says so before planning (DATA_BREAKS_RULE). Rules that only need the id (e.g. no_conflict grouped by teacher) read the item's own column and need no list. Reference lists work with every problem type; the three rules work with basic decisions only.
More examples: field maintenance — the technician the dispatcher already chose (their leave, certifications, jobs per day); surgery — the surgeon chosen at consultation (days off, qualifications, weekly hours); conference — the speaker (travel days, talks per speaker); project — the owner team (holidays, capacity); cloud — the customer account (CPU quota).
Fixed in advance (pinned) or fixed by data (reference list)?
| Pinned item (step 5) | Reference list | |
|---|---|---|
| Is that column a decision? | Yes — decided in advance for this item | No — never decided, for any item |
| Applies to | Some items; the others stay free | All items |
| Where you set it | Step 5 (pinned: true + the decided values on the item) | Step 2 (the item's column) and step 3 (the list) |
| Can the planner change it? | No in this run; yes once unpinned | Never; you edit the data |
| Rules that see it | Every rule on that decision (the pinned item occupies its machine, room, slot) | The three rules above |
| Typical reason | It already happened, or it is imposed | Someone else decided it (principal, dispatcher, surgeon) |
Choose: nobody wants the planner to choose it, for any item → reference list (a class keeps its teacher all year). The planner should choose for most items, but some are already fixed → make it a decision and pin those items (the planner picks the technician, but 3 jobs are promised to Marc). A pin fixes all decisions of the item at once.
6. Constraints
A constraint is {name, type, rule, params, weight}:
{
"name": "All tasks scheduled",
"type": "hard",
"rule": "unassigned_count",
"params": {"inverse_field": "host"},
"weight": 100
}
Constraint types
| Type | Effect |
|---|---|
hard | Each violation forces the score's hard tally negative. A feasible solution must have 0hard. |
soft | Each violation reduces the soft tally. Lower (more negative) means worse; the solver minimises it. |
medium | Reserved but not used by the built-in rules in this release. |
Constraint rules (2.0-native vocabulary)
Fourteen rules ship today. Every one of them can be declared hard or soft — the type decides whether a violation makes the solution infeasible or merely costs score.
Coverage and balance
| Rule | When to use it | params |
|---|---|---|
unassigned_count | Force every entity to be assigned. Always include one per variable. | variable_field or inverse_field (optional — for a list variable use the inverse shadow, typically "host"). |
minimize_property | Push the solver toward smaller values of a numeric property, summed across entities. | property (required). Must exist on every entity in the targeted collection. |
load_balance | Spread assignments evenly across the values of a variable. | variable_field (required). |
Conflict and eligibility
| Rule | When to use it | params |
|---|---|---|
no_conflict | Stop two entities sharing the same combination of values — the classic double-booking check. | fields (required): the list of variable ids that together must be unique. |
skill_match | Only allow a value whose capability list covers what the entity requires. | variable_field, entity_property, value_property (all required). |
resource_exclusivity | Reserve a value for entities of a matching category. | variable_field, entity_property, value_property (all required). |
resource_unavailable | Prevent assignment to a value in a slot it is unavailable for. | variable_field, slot_field, slot_property, unavailable_property (all required). |
protected_slot | Keep designated slots free unless the entity is permitted to use them. | variable_field, value_property (both required). |
Capacity, time and order
| Rule | When to use it | params |
|---|---|---|
capacity | Keep total load assigned to a value within what that value can hold. | variable_field, capacity_property (required); load_property (optional — defaults to counting entities). |
time_window | Keep an entity inside the window its assigned value allows. | variable_field, entity_start_property, entity_end_property, value_property (all required). |
precedence | Enforce that an entity is scheduled after whatever it depends on. | slot_field, depends_on_property, value_property (all required). |
no_subject_repeat_per_day | Stop the same category recurring twice in a day for one group — the timetabling spacing rule. | slot_field, group_property, category_property, day_property (all required). |
Cost and preference
| Rule | When to use it | params |
|---|---|---|
cost | Penalise by a rate carried on the assigned value. | variable_field, rate_property (required); quantity_property, scale (optional). |
preference | Reward or penalise assignments against a stated preference. | variable_field, entity_property, value_property (all required). |
Sequencing behaviour is expressed with a list variable plus derived shadows. New rules are added to the engine when a real case needs one (checklist: DEVELOPER_GUIDE.md §3.3).
7. Score
Score: 0hard / -1250soft
0hard(or nohardmention) — the solution is feasible and usable.- A non-zero hard tally — infeasible. Loosen something or accept that the problem is over-constrained.
- The soft tally is "how good" — only meaningful within the same problem; comparing soft scores across different problems isn't useful.
The 2.0 stack uses HardSoftScore. The medium band would require a different score type and isn't wired up today.
8. End-to-end — a full minimal §5bis spec
{
"problemType": "task-assignment",
"collections": {
"resources": [
{"id": "R1", "properties": {"name": "Bay-1", "startTime": "2026-06-01T08:00:00"}}
],
"tasks": [
{"id": "T1", "properties": {"name": "Cut", "priority": 1, "duration": "PT30M"}},
{"id": "T2", "properties": {"name": "Bevel", "priority": 2, "duration": "PT45M"}},
{"id": "T3", "properties": {"name": "Tack", "priority": 3, "duration": "PT20M"}}
]
},
"variables": [
{"id": "schedule", "type": "list", "on": "resources", "valueRange": "tasks"}
],
"shadows": [
{"on": "tasks", "name": "previous", "kind": "previousElement", "source": "schedule"},
{"on": "tasks", "name": "host", "kind": "inverseRelation", "source": "schedule"}
],
"derivedShadows": [
{"on": "tasks", "name": "startTime", "sources": ["previous.endTime", "host.startTime"], "expression": "previous?.endTime ?? host.startTime"},
{"on": "tasks", "name": "endTime", "sources": ["startTime", "this.duration"], "expression": "startTime + this.duration"}
],
"constraints": [
{"name": "All tasks scheduled", "type": "hard", "rule": "unassigned_count", "params": {"inverse_field": "host"}, "weight": 100},
{"name": "High-priority first", "type": "soft", "rule": "minimize_property", "params": {"property": "priority"}, "weight": 1}
],
"config": {"timeoutSeconds": 5}
}
POST that to /api/solve:
{
"score": "0hard/-6soft",
"feasible": true,
"assignments": {"R1": ["T1", "T2", "T3"]},
"shadows": {
"T1": {"previous": null, "host": "R1", "startTime": "2026-06-01T08:00", "endTime": "2026-06-01T08:30"},
"T2": {"previous": "T1", "host": "R1", "startTime": "2026-06-01T08:30", "endTime": "2026-06-01T09:15"},
"T3": {"previous": "T2", "host": "R1", "startTime": "2026-06-01T09:15", "endTime": "2026-06-01T09:35"}
}
}
Where next
- Quick Reference — the cheat sheet
- Quick Start — 4 hands-on tutorials
- Wizard User Guide — step-by-step through the UI
- API Reference — endpoint + schema details