> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prodexlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Constraints & Objectives

> Extend a planning model with rules and goals the built-in optimizer doesn't cover, authored in plain language through Dexter.

## Overview

The optimizer already enforces the physics of production without you configuring anything. Every [planning run](/product/planning-runs) respects **material balance** (inventory carried between intervals, consumed and replenished as things are made), **BOM and resource consumption** (each unit produced draws down its components and the resources its route requires), **resource capacity** as a hard ceiling, **production lead time** (output arrives some intervals after it starts), and **hard-deadline orders**. On top of that it optimizes one fixed objective: the balance between hitting demand on time and holding inventory at target — the [two built-in objectives](/product/planning-models#the-two-objectives).

**Custom constraints and custom objective terms are the extension point for everything those built-ins don't express.** They let you add rules like minimum lot sizes, a cap on how many distinct products run in an interval, minimum run lengths, or mutual exclusions — and add goals like "keep production smooth" or "penalize variety" that get weighed alongside demand and inventory. Both are authored on the [planning model](/product/planning-models#custom-constraints) and apply to every run against it.

<Note>
  This is expert functionality. Most models never need it — the built-in structure and the demand/inventory balance cover the large majority of planning problems. Reach for custom rules only when a real operating constraint has no home in the standard model.
</Note>

## Authoring with Dexter

You don't hand-write these in a form. You **describe the rule to [Dexter](/product/dexter/chat-and-tasks) in plain language** — "don't make any SKU in a batch smaller than 500 unless we're making none of it" — and Dexter expresses it as a precise constraint on the model, then validates it through the same optimizer compiler that runs at solve time. If the rule can't be expressed or doesn't compile, you find out immediately rather than at the next run.

Under the hood each rule is one entry the compiler understands: a single constraint, or a single weighted objective term. You describe the intent; Dexter owns the encoding and the fiddly numerical details. The rest of this page is a tour of what is expressible, so you know what you can ask for.

## What You Can Reference

A custom rule is built out of the optimizer's own **decision variables** — the quantities the solver is choosing. These are the only variables a rule can mention, and each is indexed the way the solver tracks it.

| Variable                  | What it represents                                                          | Indexed by         |
| ------------------------- | --------------------------------------------------------------------------- | ------------------ |
| `produced`                | How many units of a producible material to make, started in an interval     | interval, material |
| `order_interval`          | Whether a demand order is filled in a given interval (yes / no)             | order, interval    |
| `order_demand`            | Total SKU quantity demanded in an interval (derived from order assignments) | interval, SKU      |
| `start_of_interval`       | A material's inventory at the start of an interval                          | interval, material |
| `end_of_interval`         | A material's inventory at the end of an interval                            | interval, material |
| `inventory_material_loss` | How far a material's inventory sits from its target, per material           | interval, material |
| `inventory_loss`          | Aggregate inventory loss across all materials in an interval                | interval           |
| `total_inventory_loss`    | Horizon-wide inventory loss (a single number)                               | —                  |
| `total_lateness_penalty`  | Horizon-wide demand-lateness penalty (a single number)                      | —                  |

The index dimensions mean:

* **interval** — a time bucket in the horizon, numbered from `0` (the first interval).
* **material** — a material referenced by its entity name. If the entity has several variants, the rule pins the one it means with a variant condition.
* **order** — a demand order, referenced by its name.
* **SKU** — a finished-good entity.

<Note>
  **`produced` only accepts producible materials** — intermediates and SKUs. Raw materials aren't produced by the plan (they arrive as [supply](/product/demand-and-supply#supply-orders)), so a rule can't constrain "production" of a raw. Inventory variables (`start_of_interval`, `end_of_interval`) accept **any** material, raws included.
</Note>

## Building Blocks

Rules are assembled from a small, composable set of pieces. You never write these directly, but they define the boundary of what a rule can say.

**Arithmetic.** Add, subtract, and negate quantities; multiply by a constant (or by a yes/no variable); divide by a constant. Two decision variables can't be multiplied together — the model stays linear so the solver can prove optimality.

**Aggregations** roll a quantity up over a set of intervals, materials, orders, SKUs, or resources, with an optional filter to include only some of them:

| Aggregation       | What it gives you                                                                           |
| ----------------- | ------------------------------------------------------------------------------------------- |
| **sum**           | The total of a quantity over the set — e.g. everything produced of a SKU across the horizon |
| **count**         | How many conditions in the set are true — e.g. how many intervals produce a SKU at all      |
| **min** / **max** | The smallest or largest value across the set                                                |

**Shaping functions** turn a raw quantity into the shape a rule or penalty actually cares about:

| Function                 | Effect                                                                                           |
| ------------------------ | ------------------------------------------------------------------------------------------------ |
| **abs**                  | Distance from zero, ignoring sign                                                                |
| **positive part** (ReLU) | The amount above zero, and nothing below — ideal for "only the overage counts"                   |
| **dead-zone**            | Zero until a threshold, then it starts to bite — forgives small deviations                       |
| **clamp**                | Hold a value inside a floor and a ceiling                                                        |
| **piecewise-linear**     | A custom curve through points you specify — non-linear cost or reward that stays solver-friendly |
| **if / then / else**     | Pick one of two quantities depending on a condition                                              |
| **table lookup**         | Read a value out of a fixed table by position                                                    |

**Logic** composes the conditions a rule tests:

* **Comparisons** — less-than, at-most, greater-than, at-least, equal, not-equal.
* **and / or / not**, **implies** ("if A then B"), and **iff** ("A exactly when B").
* **at-most / at-least / exactly *k*** — how many of a list of conditions may (or must) hold at once.
* **for-all / exists** — a condition must hold across every member of a set, or for at least one.

## Constraint Types

A rule takes one of these shapes:

| Type              | What it does                                                                                                                        |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Simple**        | One relation that must hold — `produced of SKU X in interval 0 ≤ 5`.                                                                |
| **For-all**       | The same relation applied across an index set — the cap above, but for *every* interval.                                            |
| **Conditional**   | If a condition holds, then a relation must hold — "if this order is filled in interval 2, produce at least 100 of its SKU by then." |
| **Soft**          | A relation that *should* hold; a violation is allowed but adds a penalty to the objective instead of forbidding the plan.           |
| **All-different** | A set of quantities must all take distinct values.                                                                                  |
| **Cardinality**   | At most / at least / exactly *k* of a set of relations may hold.                                                                    |

### Hard vs. soft

A **hard** constraint is inviolable: the solver will not return a plan that breaks it, and if no plan can satisfy it the run comes back [infeasible](/product/planning-runs#when-a-run-is-infeasible). Use hard constraints for genuine physical or contractual limits.

A **soft** constraint is a preference with a price. The plan is *allowed* to break it, but every violation adds a penalty to the objective, so the optimizer avoids breaking it unless doing so buys enough elsewhere. The penalty comes in two styles:

* **Per-unit** — the cost scales with *how much* the rule is missed (each unit over a cap costs the penalty).
* **Fixed** — a flat cost the moment the rule is broken at all, regardless of by how much.

Softening a rule is the usual fix when a hard constraint makes runs infeasible but the underlying preference is real — you keep the pressure without the cliff.

## Custom Objective Terms

Beyond constraints, you can add **weighted terms to the objective itself**. A term is an expression plus a weight and a direction (minimize or maximize); it's folded in alongside the two built-in terms (demand lateness and inventory loss), so the optimizer balances your goal against the standard ones automatically.

This is how you express goals the defaults don't carry — minimize a cost proxy, reward smoother output, or penalize something the built-ins are indifferent to. The weight sets how loudly the term speaks relative to demand and inventory; a small weight nudges the plan, a large one dominates it.

<Tip>
  Constraint or objective term? If the rule is a line the plan must not cross, make it a **constraint**. If it's a preference you want the plan to lean toward but not at any cost, make it a **soft constraint** or an **objective term** — those let the optimizer trade it off instead of failing.
</Tip>

## Worked Examples

Three rules that show the range, each grounded in the variables above. You'd ask for these in plain language; the description of each rule's shape shows what Dexter builds.

### Minimum lot size

> "Don't produce any SKU in a quantity below its minimum lot size — but zero is fine."

For each interval, a rule reads `produced of the SKU = 0` **or** `produced of the SKU ≥ 500`. It's a **for-all** over intervals wrapping an **or** of two comparisons on `produced`, which forbids the awkward middle ("a handful of units") while still allowing the plan to skip the SKU entirely. Make it a **soft, per-unit** rule instead and sub-minimum batches become merely expensive rather than impossible.

### Cap distinct products per interval

> "Never run more than three different products in a single interval."

Production is planned per material per interval, so this counts variety within a time bucket. For each interval, a rule reads `count of producible materials with produced ≥ 1  ≤  3` — a **count** aggregation over producible materials, compared against 3, applied **for all** intervals. It curbs changeovers by limiting how many things are in flight at once.

### Reward longer runs (an objective term)

> "All else equal, prefer plans that run fewer distinct products per interval."

The same distinct-product count, summed across the whole horizon, becomes a **minimize** objective term with a **small weight**. It doesn't forbid variety the way the cap does — it gently biases the optimizer toward consolidating production, and yields whenever hitting a due date or an inventory target is worth the extra changeover.

## Good to Know

* **Every rule is validated by the optimizer's compiler.** A rule that can't be expressed is rejected when you author it; a rule that compiles but conflicts with the plan surfaces when the run executes. See [validating inputs](/product/planning-runs#validating-inputs).
* **An over-tight set can make a run infeasible.** A minimum-production floor stacked against a maximum-output cap can together exclude every possible plan; the run then comes back infeasible with no solution. Loosen the binding rule, or soften it. The full recovery playbook is in [when a run is infeasible](/product/planning-runs#when-a-run-is-infeasible).
* **Change one rule at a time.** Because custom rules interact with each other and with the built-in structure, add or tighten them incrementally and re-run, so you can see which one moved the plan — or broke it.
* **Dexter handles the numerical subtleties.** Some encodings are only sound on integer quantities like `produced` (exact-equality and not-equal tests don't apply cleanly to continuous inventory levels, which are compared with ranges instead). You describe the intent in plain language; Dexter picks an encoding the solver can handle.
