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 — API Reference

Base URL (production): https://api.planningpowertools.com Base URL (local dev): http://localhost:8001

The 2.0 API exposes five endpoints. POST /api/solve runs a full solve synchronously and returns the result envelope on the same response. POST /api/validate checks a problem without solving it. POST /api/check looks for data that makes a valid plan impossible ("can this work?"). GET /api/version reports the build. GET /api/health is the liveness probe.

There is no asynchronous job API: a solve answers on the same request, and nothing is stored on the server afterwards.


GET /api/health

Returns the standard PDP health envelope.

Response 200:

{
  "status": "UP",
  "version": "2.0.0",
  "timestamp": "2026-06-01T08:00:00Z"
}

POST /api/solve

Submit a §5bis problem JSON. The server parses it, generates per-schema domain classes (cached after the first call), runs the solver within the configured time budget, and returns the solved envelope.

Content-Type: application/json

Request body — §5bis schema

{
  "problemType": "task-assignment",       // descriptive label
  "collections": {                         // REQUIRED — at least one host + one entity collection
    "<name>": [
      {"id": "<id>", "properties": { /* free-form */ }}
    ]
  },
  "variables": [                           // REQUIRED — exactly one list variable supported today
    {
      "id":         "<variable-id>",
      "type":       "basic" | "list",      // NO "chained" — removed in 2.0
      "on":         "<host-collection>",
      "valueRange": "<entity-collection>"
    }
  ],
  "shadows": [                             // optional — built-in list shadows
    {"on": "<entity-collection>", "name": "<shadow-name>",
     "kind": "previousElement" | "nextElement" | "inverseRelation" | "index",
     "source": "<variable-id>"}
  ],
  "_meta": {"problemName": "<name shown in the wizard>"},   // optional — display only
  "derivedShadows": [                      // optional — supplier expressions
    {"on": "<entity-collection>", "name": "<shadow-name>",
     "sources": ["<path>", "<path>"],
     "expression": "<DSL expression>"}
  ],
  "constraints": [                         // optional but recommended
    {"name": "<name>", "type": "hard" | "soft", "rule": "<rule>",
     "params": { /* per-rule */ }, "weight": <int>}
  ],
  "config": {                              // optional — every key is optional
    "timeoutSeconds": <1..600>,            // default 30; the solve stops after this many seconds
    "unimprovedSeconds": <1..timeoutSeconds>, // stop early after this many seconds without improvement
    "maxSteps": <int ≥ 1>,                 // cap on local-search steps
    "reproducible": true | false,          // default true: same input + same seed = same plan
    "randomSeed": <64-bit int>,            // default 0; only allowed when reproducible is true
    "constructionHeuristic": "FIRST_FIT" | "FIRST_FIT_DECREASING" | "WEAKEST_FIT" | "WEAKEST_FIT_DECREASING"
                           | "STRONGEST_FIT" | "STRONGEST_FIT_DECREASING" | "CHEAPEST_INSERTION"
                           | "ALLOCATE_ENTITY_FROM_QUEUE" | "ALLOCATE_TO_VALUE_FROM_QUEUE" | "ALLOCATE_FROM_POOL",
    "sortEntitiesBy": "<numeric item property>",          // bigger = placed first
    "sortValuesBy": {"<basic variable id>": "<numeric value property>"},  // bigger = stronger
    "localSearch": "HILL_CLIMBING" | "TABU_SEARCH" | "SIMULATED_ANNEALING" | "LATE_ACCEPTANCE"
                 | "GREAT_DELUGE" | "VARIABLE_NEIGHBORHOOD_DESCENT"
                 | "LATE_ACCEPTANCE_THEN_TABU_SEARCH",  // Late Acceptance until feasible, then Tabu Search
    "startingTemperature": "<hard>hard/<soft>soft"        // required by, and only allowed with, SIMULATED_ANNEALING
  }
}

Planning period of a weekly timetable — dates on the time slots (display only)

When the time slots are one week that repeats (a school timetable, recurring shifts), each slot lists in a dates property every date it takes place on, e.g. "dates": ["2026-09-07", "2026-09-14", ...] (its weekday in the period, minus closed days). The solver ignores it; the Results page ("Who does what") and the Excel export lay the week onto those dates: the whole period, week by week, with the closed days shown, and a Calendar sheet with every dated occurrence. The wizard's slot generator writes dates when "Same timetable every week of the period" is ticked (with optional closed days). Slots without dates are shown on their own dates.

Search settings — what is accepted

Every setting is really applied. A setting the chosen search would ignore is refused (400 BAD_CONFIG), never silently dropped:

  • *_DECREASING needs sortEntitiesBy, a numeric property present on every item.
  • WEAKEST_* and STRONGEST_* need a sortValuesBy entry for every basic variable, each naming a numeric property present on every value.
  • sortEntitiesBy is refused with FIRST_FIT, WEAKEST_FIT and STRONGEST_FIT, because it would have no effect there. Likewise, sortValuesBy is refused with FIRST_FIT and FIRST_FIT_DECREASING.
  • Without constructionHeuristic, a sort setting alone still orders the construction: sortEntitiesBy places the largest item first, and sortValuesBy tries the weakest value first.
  • Combinations that cannot work on the problem's shape are refused, with the reason:
Problem shapeConstruction heuristics acceptedLocal searches accepted
Basic, 1 variableall 10all 7
Basic, 2+ variablesall except ALLOCATE_TO_VALUE_FROM_QUEUEall 7
List onlyFIRST_FIT, FIRST_FIT_DECREASING (list construction always places items one by one in order; sortEntitiesBy is the only way to change that order)all except VARIABLE_NEIGHBORHOOD_DESCENT
List + basicall except CHEAPEST_INSERTION and ALLOCATE_FROM_POOL (and except ALLOCATE_TO_VALUE_FROM_QUEUE with 2+ basic variables)all except VARIABLE_NEIGHBORHOOD_DESCENT

LATE_ACCEPTANCE_THEN_TABU_SEARCH runs two local-search phases: Late Acceptance until every hard rule holds, then Tabu Search for the time left (maxSteps bounds the second phase). On large problems Late Acceptance reaches a valid plan sooner, and Tabu Search improves its soft score better. If no valid plan is found, the first phase uses the whole time limit.

Combined moves: on a problem with two or more basic variables (for example machine and time), local search also changes every variable of one item in a single move, whatever the search settings; an item on an option that does not suit it often needs two decisions to change together.

Group moves: on a basic-variable problem with a same_value_in_group rule, local search also moves a whole group at once (every unpinned item of the group gets the same new value), whatever the search settings. A group with a pinned item only moves its other items to the pinned value.

Reproducibility: reproducible: true runs Timefold in PHASE_ASSERT mode with the seed, so the same input and the same seed give the same plan. If a time limit could stop the run at different points, use maxSteps to bound it. reproducible: false runs in NON_REPRODUCIBLE mode.

Forbidden keys (cause HTTP 400)

Forbidden top-level keyMigration
entitiesMove into collections.<entity-collection-name>.
valueRangesEach entry becomes a top-level collection under collections.
schedulingPattern: "TIME_GRAIN"Drop. Add the canonical startTime/endTime derived shadows.
timeGrainConfigDrop. Same migration as schedulingPattern.
chainedConfigVariable type chained is gone. Use list + derived shadows.

Variable rules

  • type must be basic or list. Anything else → HTTP 400.
  • on and valueRange must reference existing keys of collections.
  • Today the solver supports exactly one list variable per problem (multiple basic variables are also accepted).

Built-in shadow rules

  • kind ∈ {previousElement, nextElement, inverseRelation, index}.
  • source is the id of an existing list variable.
  • on is the entity collection (the same collection that's the valueRange of the list variable).

Derived-shadow rules

  • sources is non-empty array of path strings. Every path the expression reads must appear here.
  • expression is the DSL described in the Quick Reference. Roots must be one of: this, previous, next, host, index, or another declared shadow's name. Unprefixed identifiers (e.g. just duration without this.) are rejected.

Constraint rules (active 2.0 vocabulary)

Each constraint names a rule, a type ("hard" or "soft"), a whole-number weight (hard 1..1,000,000; soft 0..1,000,000) and a params object. The hard/soft level is set per constraint by type. An unknown rule returns HTTP 400 listing the accepted rules.

The rules, their params and exactly how each violation is counted are in the generated rule reference. It is built from the engine's rule catalogue (powersolver-api/src/main/resources/rules/catalog.json), which is the single source of truth: the engine, the score breakdown, the validator and the independent oracle all follow it. Two catalogue-wide statements apply to every rule. First, only fully assigned items are evaluated; unassigned items are counted only by unassigned_count. Second, a set of value conventions covers lists, comparison as text, and times.

Representative examples:

{"name": "Every case assigned",  "type": "hard", "rule": "unassigned_count", "params": {"variable_field": "officer"}, "weight": 100},
{"name": "Officer has skill",    "type": "hard", "rule": "skill_match",      "params": {"variable_field": "officer", "entity_property": "required_skill", "value_property": "skills"}, "weight": 100},
{"name": "Caseload capacity",    "type": "hard", "rule": "capacity",         "params": {"variable_field": "officer", "capacity_property": "caseload_max"}, "weight": 100},
{"name": "Within SLA window",    "type": "hard", "rule": "time_window",      "params": {"variable_field": "slot", "value_property": "startTime", "entity_start_property": "startTime", "entity_end_property": "endTime"}, "weight": 100},
{"name": "Dependencies in order","type": "hard", "rule": "precedence",       "params": {"slot_field": "slot", "value_property": "startTime", "depends_on_property": "dependsOn"}, "weight": 100},
{"name": "Minimise cost",        "type": "soft", "rule": "cost",             "params": {"variable_field": "adjuster", "rate_property": "hourlyRate"}, "weight": 3},
{"name": "Shift preference",     "type": "soft", "rule": "preference",       "params": {"variable_field": "nurse", "entity_property": "shiftType", "value_property": "preferredShiftType"}, "weight": 5}

Response envelope

Every endpoint except /api/health and /api/version answers with the PDP response envelope:

{
  "jobId": "jobId_1791701089302",          // identifies the call for audit/tracing (not pollable)
  "producer": "PowerSolver",
  "status": "success" | "partial" | "failed",
  "timestamp": "2026-10-11T06:44:49.304Z",
  "execution_time_ms": 1234,
  "payload": { /* see below */ },
  "errors": [ {"code": "...", "message": "...", "recoverable": true, "path": "..."} ],
  "trace": {"caller": "self" | "<caller>", "call_context": "standalone" | "orchestrated"}
}
HTTPstatusWhenerrors
200successThe plan breaks no hard rule (payload.feasible is true).[]
200partialThe plan is not usable as it is: it breaks at least one hard rule, and/or the time limit ended the search before every item had a place. The full plan is still in payload (feasible: false); do not use it as is.TIME_LIMIT — "The planner placed <n> of <total> items before the time limit ran out …" (PowerSolver never leaves an item unplaced on purpose); INFEASIBLE — "<n> must-follow rule(s) broken: <rule name> (<matches>), …", or, when no rule shows a count, "The plan breaks a must-follow rule (score <score>) …"
400failedThe problem is invalid. Nothing was solved.one entry per issue: {code, message, recoverable: true, path}
429failedAll solve slots are busy (powersolver.limits.max-concurrent-solves, default 4). Header Retry-After: 30.code BUSY
501failedA declared but unsupported feature, for example more than one list variable.code NOT_IMPLEMENTED
500failedAn unexpected internal error.code INTERNAL_ERROR

Validation issue codes are stable; wizards and the orchestrator rely on them: UNKNOWN_FIELD, UNKNOWN_RULE, RULE_NOT_FOR_VARIABLE_TYPE, MISSING_PARAM, UNKNOWN_PARAM, UNKNOWN_VARIABLE, UNKNOWN_PROPERTY, WRONG_VALUE_TYPE, MISSING_VALUE, UNKNOWN_REFERENCE, BAD_WEIGHT, UNSUPPORTED_SEVERITY, EMPTY_VALUE_POOL, DUPLICATE_ID, BAD_CONFIG, TOO_LARGE, BAD_RULE_NAME (a rule name must be plain ASCII letters/digits, spaces and _ - ' ( ) ., start with a letter or digit, and not be null/nil), DUPLICATE_RULE_NAME, CONFLICTING_PINS (a same_value_in_group must-follow rule whose group members are fixed to different values). A JSON shape error, such as a wrong type, is reported as BAD_REQUEST. path points at the offending spot, for example constraints[2].params.entity_property or config.timeoutSeconds.

Example 400, for a misspelled property:

{"status": "failed", "payload": {},
 "errors": [{"code": "UNKNOWN_PROPERTY", "path": "constraints[0].params.entity_property", "recoverable": true,
             "message": "Rule 'Skills': entity_property 'nede' is not a property of any item in 'items'. …"}]}

Payload of a solve (success and partial)

{
  "score":    "0hard/-12soft",            // textual HardSoftScore, recomputed from scratch after the solve
  "feasible": true,                        // true if and only if the hard part is 0
  "assignments": { "<hostId>": ["<entityId>", "..."] },  // list mode; basic mode: per-item values
  "shadows": { "<entityId>": { "<shadow-name>": "<stringified value>" } },   // when the problem has shadows
  "shadowNumbers": { "<entityId>": { "<shadow-name>": 0.75 } },          // typed numeric shadows (durations in hours)
  "scoreBreakdown": [
    {"name": "All tasks assigned", "rule": "unassigned_count", "type": "hard", "weight": 100, "matches": 0, "penalty": 0},
    {"name": "Prefer high-priority tasks", "rule": "minimize_property", "type": "soft", "weight": 1, "matches": 5, "penalty": 15}
  ],
  "violations": { "<hard rule name>": 0 }   // the hard rows of scoreBreakdown as {name: matches}
}
  • scoreBreakdown has one row per constraint, in request order. matches and penalty are computed independently of the solver, from the rule's counting text in the rule reference. The hard penalties add up to the hard part of score, and the soft penalties to the soft part. The test suites and the quality gate check this on every shipped and generated problem.
  • shadowNumbers exists so a client, or the independent oracle, can recheck numeric rules without parsing the stringified shadows.

How shadow values are written as text:

Value typeEmitted as
nullnull
Host or element instance (previous / next / host)The instance's id string
LocalDateTimeISO-8601 e.g. "2026-06-01T08:30"
DurationISO-8601 e.g. "PT45M"
Number / Stringvalue.toString()

POST /api/validate

Takes the same body as /api/solve. It runs exactly the same validation, including the server limits, and never solves. It always answers HTTP 200 with status: "success":

{"status": "success",
 "payload": {"valid": false,
             "issues": [{"code": "BAD_CONFIG", "path": "config.timeoutSeconds",
                         "message": "config.timeoutSeconds is 601; this server allows at most 600 seconds per solve. Lower it."}]}}

valid: true comes with issues: [].


POST /api/check — "Can this work?"

Takes the same body as /api/solve, runs the same validation, then looks for data that makes a valid plan impossible or doubtful. It never solves and always answers HTTP 200 with status: "success":

{"status": "success",
 "payload": {"verdict": "IMPOSSIBLE",
             "findings": [{"level": "IMPOSSIBLE", "code": "PINS_BREAK_RULE", "rule": "No room twice",
                           "message": "The fixed (pinned) items already break this must-follow rule 1 time(s) among themselves; ...",
                           "items": []}],
             "issues": [],
             "checks": ["DATA_BREAKS_RULE", "EMPTY_WINDOW", "DEPENDENCY_LOOP", "PINS_BREAK_RULE", "NO_OPTION_LEFT", "NO_PAIR_LEFT",
                        "NOT_ENOUGH_PLACES", "TIGHT_CAPACITY", "CHAIN_TOO_LONG", "CHAIN_NO_SLACK"]}}
verdictMeaning
INVALIDThe problem cannot be read: issues holds the same issues as /api/validate; no check ran.
IMPOSSIBLECertain: no plan can respect every must-follow rule.
AT_RISKDoubtful: a resource is 85 % full or more for a family of 10 items or more.
NO_PROBLEM_FOUNDNo check in checks found a problem. This is not a guarantee that a valid plan exists.
Finding codeLevelMeaning
EMPTY_WINDOWIMPOSSIBLEAn item's start bound is later than its end bound (time_window): no time fits. items lists them.
DEPENDENCY_LOOPIMPOSSIBLEItems wait for each other in a loop (precedence). items is the loop, first item repeated.
PINS_BREAK_RULEIMPOSSIBLEThe pinned items already break a must-follow rule among themselves, counted as the engine counts (rules where such a break can never be repaired: no_conflict, skill_match, protected_slot, resource_unavailable, capacity, time_window, precedence, no_subject_repeat_per_day, same_value_in_group).
NO_OPTION_LEFTIMPOSSIBLEA free item has no option left for one decision once the rules about the item alone are applied (skill_match, protected_slot, time_window, capacity smaller than its load, unavailability of its fixed resources). The message says which rule leaves how many options.
NO_PAIR_LEFTIMPOSSIBLEFor a resource_unavailable rule on two decisions (a resource and a time): every option that suits the item is unavailable at every time it can use.
NOT_ENOUGH_PLACESIMPOSSIBLESupply against demand (Hall's condition): the items that can only use a set of places outnumber those places. Places are the value combinations of a no_conflict rule (per group when group_property is set) minus the pairs a resource_unavailable rule forbids; the days of a no_subject_repeat_per_day rule (per group and category); or, for capacity, the summed capacity against the summed loads. Pinned items take their own place. The message names the options (e.g. "room Grand amphithéâtre, Amphithéâtre A x slot 16 of 20") and lists the items.
TIGHT_CAPACITYAT_RISKThe same counting, 85 % full or more, for a family of at least 10 items (not only pinned ones). Not impossible, but the search may fail or take long.
CHAIN_TOO_LONGIMPOSSIBLEA chain of dependent items (precedence) does not fit in the times its items may take (their windows and closed times), even ignoring every resource: walking the chain in order, an item has no possible time after the earliest time of its prerequisite. items is the chain.
CHAIN_NO_SLACKAT_RISKThree or more items in dependency chains have exactly one possible time left once their chain and window are taken into account: any delay breaks the chain.
DATA_BREAKS_RULEIMPOSSIBLEA must-follow rule that reads only data (skill_match or capacity with resource_property: a resource fixed on each item) is broken by the data itself, so every plan breaks it. params: kind (mismatch: items whose resource lacks what they need; overload: resources given more than their capacity, over = "id +excess").

Each finding also has params: the same facts as data (count, demand, supply, percent, chain, reasons, where with the options used, group…), so a client can write the sentence in its own language from code and params; message stays the English sentence. The wizard does this in 5 languages.

Only must-follow (hard) rules are checked.

Quick plan — POST /api/check?trialSeconds=N. When the verdict is NO_PROBLEM_FOUND or AT_RISK, the engine also runs a short solve (N seconds, 1 to 60, never longer than the problem's own timeoutSeconds; it counts against max-concurrent-solves) and adds trial to the payload:

"trial": {"ran": true, "seconds": 20, "validPlanFound": true, "score": "0hard/-12soft"}
"trial": {"ran": false, "reason": "BUSY", "message": "..."}

validPlanFound: true proves a valid plan exists (every must-follow rule holds and every item is placed). false does not mean impossible: only IMPOSSIBLE says that. No trial is run for INVALID or IMPOSSIBLE. The wizard's step 7 button Try a quick plan (20 s) uses it.


Stored problems and solutions

The engine keeps the plans made in the wizard: one SQLite file, /data/powersolver.db, on the powersolver-data volume. POST /api/solve stores nothing (it is unchanged); the endpoints below store, list and manage problems and their solutions. Every answer is the usual PDP envelope; the caller's name comes from the X-PPT-User header (else the body's caller, else api) and is written to each row's history.

No sign-in check yet. Anyone who can reach the API can list, change and remove stored plans. This is an accepted risk until the product has its own authentication; keep backups (installation guide).

MethodPathDoes
GET/api/problems?query=&status=&include_deleted=&limit=50&after=list: items[] (name, type, status DRAFT / SOLVED, dates, who, solution_count, best_score, last_solved_at), next (cursor for after)
POST/api/problemsstore {name, description?, problem, source?} → 201, the summary
GET/api/problems/{id}the summary, problem (the JSON), problem_sha256, source, solutions[]
PUT/api/problems/{id}change name, description and/or problem
POST/api/problems/{id}/copya new problem from this one ({name?}) → 201
POST/api/problems/{id}/solvesolve the stored problem exactly as /api/solve does and keep the solution; the payload gets solution_id and problem_id
GET/api/problems/{id}/solutions, /api/solutions/{id}the solutions; one solution with solution and problem (the problem as it was solved)
DELETE/api/problems/{id}, /api/solutions/{id}delete (hidden, can be restored); ?purge=true on a deleted one removes it for good
POST/api/problems/{id}/restore, /api/solutions/{id}/restoreundo a delete
GET/api/problems/{id}/audithistory of the problem and its solutions, newest first (actor_verified is 0: names are declared, not checked)
POST/api/problems/import{name?, problem, solution?}: store the two files of a run made before plans were kept

A solution is stored for a usable plan (success) and for an incomplete or infeasible one (partial); a refused problem stores nothing. Each solution records the hash of the problem it answers: once the problem is changed, its older solutions are marked stale: true and keep the problem as it was. best_score compares hard, then medium, then soft. Errors: NOT_FOUND 404, NOT_DELETED 409 (purge or restore of a row that is not deleted), TOO_LARGE 413 (a JSON over powersolver.store.max-json-mb, 50 MB), BAD_REQUEST 400.

GET /api/version

{"version": "2.0.0", "timefold": "2.0.0", "builtAt": "2026-10-11T06:44:15.901Z"}

version and builtAt come from the build (build-info). A build without build info answers "version": "dev" and "builtAt": null.


Server limits

Property (application.yml)DefaultEffect
powersolver.limits.max-items5000A collection with more items → 400 TOO_LARGE.
powersolver.limits.max-timeout-seconds600A config.timeoutSeconds above it → 400 BAD_CONFIG.
powersolver.limits.max-concurrent-solves4Any further solve → 429 BUSY, Retry-After: 30. /api/validate is not limited.

Score size: scores and every rule's penalty are 64-bit (Timefold 2.0 HardSoftScore is long-based; since 2026-10-12 each rule also computes its penalty in 64-bit). At the maximum weight (1,000,000) a single item can be worth about 9 trillion before any limit. The capacity rule reads each item's load as a whole number below 2,147,483,648.


Worked example — back-to-back task assignment

Request:

POST /api/solve
Content-Type: application/json

{
  "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}
}

Response payload (inside the envelope; scoreBreakdown and violations omitted here):

{
  "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"}
  }
}

CORS

powersolver-api's CorsConfig.java allows:

  • http://localhost:* (any local-dev origin)
  • https://*.planningpowertools.com (production wizards, ad-hoc tooling)

Methods: GET, POST, OPTIONS, DELETE. No credentials. Tighten if/when a non-PDP caller appears.


Caching behaviour

On the first POST for a given schema, DomainAssembler builds dynamic host/element/solution classes via ByteBuddy and DynamicConstraintProviderFactory builds a parameterised ConstraintProvider. Both are cached process-globally keyed by a SHA-256 schema hash. Subsequent POSTs with the same schema hit the cache — class generation cost is amortised.

If a redeploy invalidates the JVM, the cache rebuilds on the next POST. Container restart is the only way to evict.