PowerSolver Docs
Back to Wizard
This page belongs to the full documentation. You don't need it to plan: back to the essentials.

PowerSolver 2.0 — Concepts Guide

Read this once. Everything else in the docs assumes you understand the vocabulary here.


1. The five core nouns

TermWhat it is
CollectionA named pool of items the solver knows about (e.g. resources, tasks). Each item has an id and a properties bag.
VariableAn assignment relation. One of two types: basic (one-to-one) or list (one host owns an ordered list).
EntityAn item the solver makes decisions about — gets placed in a list, or gets a value assigned to it.
HostFor a list variable: the collection that OWNS the lists. Each host item carries its own ordered list of entities.
ConstraintA scoring rule. Each is hard (must-satisfy) or soft (optimise).

Two more nouns that the solver fills in automatically:

TermWhat it is
Built-in shadowA reactive read-only value the solver maintains on every entity in a list: previous, next, host, index.
Derived shadowA 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 kindWhat it returnsTypical use
previousElementThe element BEFORE this one in its host's list (or null if first).Computing back-to-back start times: previous?.endTime.
nextElementThe element AFTER this one in its host's list (or null if last).Look-ahead constraints.
inverseRelationThe host that owns the list containing this element.Cross-referencing host-level facts: host.startTime.
indexThe 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"
  }
]
  • startTime for the FIRST task on a host = the host's own startTime (from the host's properties bag). For any subsequent task = the previous task's endTime.
  • endTime = startTime + the task's own duration property (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:

ConstructMeaning
propertyNameA reference to a known shadow / variable name on this entity.
path.to.propertyDotted access — each segment must appear in the sources array.
a?.bNull-safe access — if a is null, the whole expression evaluates to null instead of throwing.
a ?? bNull-coalesce — returns a if non-null else b.
a + b, a - b, a * b, a / bArithmetic. Operates per Java semantics on Number, Duration, LocalDateTime.
this.propertyNameExplicit 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:

KindRoleJob-shop exampleSchool example
Items to planWhat gets planned (the on of the decisions)operationslessons
Options of a decisionWhat the planner chooses from (a decision's valueRange)machines, time slotsrooms, time slots
Reference listData the rules look up; nothing is chosen from itoperatorsteachers

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:

RuleChecks against the reference list
resource_unavailableThe item is not placed at a time its person or thing is away
skill_matchIts person or thing has what the item needs (e.g. a certification)
capacityIts 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 itemNo — never decided, for any item
Applies toSome items; the others stay freeAll items
Where you set itStep 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 unpinnedNever; you edit the data
Rules that see itEvery rule on that decision (the pinned item occupies its machine, room, slot)The three rules above
Typical reasonIt already happened, or it is imposedSomeone 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

TypeEffect
hardEach violation forces the score's hard tally negative. A feasible solution must have 0hard.
softEach violation reduces the soft tally. Lower (more negative) means worse; the solver minimises it.
mediumReserved 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

RuleWhen to use itparams
unassigned_countForce 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_propertyPush the solver toward smaller values of a numeric property, summed across entities.property (required). Must exist on every entity in the targeted collection.
load_balanceSpread assignments evenly across the values of a variable.variable_field (required).

Conflict and eligibility

RuleWhen to use itparams
no_conflictStop 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_matchOnly allow a value whose capability list covers what the entity requires.variable_field, entity_property, value_property (all required).
resource_exclusivityReserve a value for entities of a matching category.variable_field, entity_property, value_property (all required).
resource_unavailablePrevent assignment to a value in a slot it is unavailable for.variable_field, slot_field, slot_property, unavailable_property (all required).
protected_slotKeep designated slots free unless the entity is permitted to use them.variable_field, value_property (both required).

Capacity, time and order

RuleWhen to use itparams
capacityKeep 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_windowKeep an entity inside the window its assigned value allows.variable_field, entity_start_property, entity_end_property, value_property (all required).
precedenceEnforce that an entity is scheduled after whatever it depends on.slot_field, depends_on_property, value_property (all required).
no_subject_repeat_per_dayStop 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

RuleWhen to use itparams
costPenalise by a rate carried on the assigned value.variable_field, rate_property (required); quantity_property, scale (optional).
preferenceReward 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 no hard mention) — 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