Install
$ agentstack add skill-relationalai-rai-agent-skills-rai-prescriptive-problem-formulation ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Problem Formulation
> Requires relationalai>=1.11.0. The dual-guided multi-objective methods in this skill read solver sensitivity via solve("highs", sensitivity=True) (shadow prices as exact frontier slopes); earlier versions reject the request at the solver and omit the dual fields on solve_info(). See rai-setup.
Summary
What: Optimization formulation — decision variables, constraints, objectives, and common problem patterns. Assumes a problem has already been selected via discovery.
When to use:
- Formulating variables, constraints, and objectives for a selected problem
- Reviewing or validating an existing formulation
- Translating business requirements into mathematical formulation
- Debugging formulations that would produce trivial or infeasible solutions (missing constraints, conflicting bounds, wrong aggregation scope)
- Choosing between variable types (continuous, integer, binary)
- Designing multi-concept coordination (flow networks, selection + quantity)
When NOT to use:
- Post-solve diagnosis of solutions that look wrong (all zeros, infeasible status, concentrated values) — see
rai-prescriptive-results-interpretation - Question discovery (what can this ontology answer, reasoner classification) — see
rai-discovery - PyRel syntax (imports, types, property patterns, stdlib) — see
rai-pyrel-coding - Ontology modeling or model enrichment (concept design, gap classification) — see
rai-ontology-design - Solver execution and diagnostics (solver selection, parameters, numerical stability) — see
rai-prescriptive-solver-management - Aggregation syntax (count/sum/per patterns) — see
rai-querying
Overview:
- Ground in the base ontology via
relationalai.semantics.inspect.schema(model)— concepts, properties, types, relationships you're about to reference - Define decision variables (type, bounds, scope, naming)
- Define constraints (forcing, capacity, balance, linking; validate interactions)
- Define objective (direction, coefficients, multi-component handling)
- Validate the complete formulation (structure, completeness, feasibility, data) — includes pre-solver audit that decision variables / constraints / objectives registered correctly
- Solve and refine — diagnose surprising results via targeted
display(ref)/verify/solve_info, simplify once correct, re-solve - Post-solve refinement — present results, surface user reactions, iterate on the formulation (see Workflow Step 7)
Quick Reference — Problem API
from relationalai.semantics.reasoners.prescriptive import Problem
from relationalai.semantics.std import aggregates as aggs
# Problem(model, Float) for MIP-style; Problem(model, Integer) for CSP-style (see [csp-formulation.md](references/csp-formulation.md)).
problem = Problem(model, Float)
# Decision variable — prefer scoped form (where=[...]) over unscoped.
# Capture the returned ref for targeted diagnostics later (model.select(x_flow.name, x_flow.lower, x_flow.upper).to_df(), etc).
x_flow = problem.solve_for(
Lane.flow,
where=[Lane.active],
name=["item_id", "src_id"],
lower=0.0,
type="cont",
)
# Constraint — capture the ref to inspect just this constraint's grounded form
cap = problem.satisfy(
model.require(
aggs.sum(Lane.flow).per(Source).where(Lane.from_source(Source)) =1.13*) enum members, rendered by member name; on 1.12.0 itself pass the Python-side string `Member.name` instead (a member fails type inference there). |
| `satisfy` | `(expr, name=, keyed_by=)` | Add constraint. Returns `ProblemConstraint` — capture this ref to inspect the constraint's grounded form via `problem.display(ref)`. `keyed_by={"key": Concept}` declares the family's grounding keys as identifying entity back-pointers for post-solve readback — see `references/constraint-formulation.md` > Declaring constraint keys for post-solve readback. |
| `minimize` | `(expr, name=)` | Set minimization objective. Returns `ProblemObjective` — capture for targeted `display(ref)`. |
| `maximize` | `(expr, name=)` | Set maximization objective. Returns `ProblemObjective` — capture for targeted `display(ref)`. |
| `solve` | `(solver, time_limit_sec=, print_format=, ...)` | Execute solve. Solvers: `"highs"`, `"minizinc"`, `"ipopt"`, `"gurobi"`. `print_format` (`"moi"`, `"latex"`, `"mof"`, `"lp"`, `"mps"`, `"nl"`) populates `solve_info().printed_model` |
| `solve_info` | `()` | Post-solve summary (`termination_status`, `objective_value`, `solve_time_sec`, `num_points`, `solver_version`, `error`, `printed_model`). Has `.display()` method. |
| `verify` | `(*fragments)` | Post-solve check that the returned solution satisfies the original `Fragment`s at IC strictness (tighter than solver tolerance). Pass the original `model.require(...)` values, not `ProblemConstraint` refs. |
| `display` | `(part=None, *, where=None, limit=None, print_output=True)` | Print materialized formulation. `display()` for everything; `display(ref)` for one constraint or objective; `display(ref, where=)` scopes to filter-matching rows (`where=` requires `part`); `display(..., limit=N)` caps each table at top-N rows by `.name`. Variable subconcepts raise — query rows via `model.select(var.name, var.lower, var.upper).to_df()`. |
| `num_variables` / `num_constraints` / `num_min_objectives` / `num_max_objectives` | `()` | Engine-queryable counts; usable inside `model.require(...)` to assert formulation cardinality before solve |
| `Variable.values` | `(sol_index, value_ref)` | Property on `ProblemVariable`. Extracts solution values at `sol_index` (0-based), binding each value to `value_ref` (a `Float.ref()` or `Integer.ref()`). Use inside `model.select(...).where(var.values(sol_index, value_ref))`. Primary pattern for `populate=False` workflows. |
| `problem.variables` / `problem.constraints` / `problem.objectives` | (attributes) | Lists of registered refs in declaration order — iterate to walk an unfamiliar Problem |
---
## Formulation Workflow
**Interaction mode:** Before starting, ask the user which mode they prefer:
- **Guided** — present your proposed variables, constraints, and objective at each step and confirm before proceeding. Best when the user has domain context to share — problem framing involves subjective judgment (what's a hard constraint vs. a soft goal, how to scope variables, which business rules matter most).
- **One-shot** — produce the best formulation you can in a single pass. Best when the user wants speed and will review/iterate after.
After a question is selected (from question discovery) and the ontology is enriched (if needed), build the formulation in this order:
### Step 1: Ground in the base ontology
Prescriptive formulations reference concepts, properties, and relationships from an existing model. Before writing `solve_for` / `satisfy` / `minimize` / `maximize`, confirm every name and type you're about to use against the real schema:
```python
from relationalai.semantics import inspect
schema = inspect.schema(model)
# Every concept referenced in the formulation
for name in referenced_concepts:
assert name in schema, f"Concept {name} not in model"
props = schema[name].properties
# surface any properties used in bounds, constraints, or the objective
Catches two silent-failure modes that account for most prescriptive errors:
- Hallucinated surface —
Customer.tierin a constraint when the real property isCustomer.category. Solver happily runs on wrong variables and returns nonsense. - Wrong-type inference — using an
Integerproperty as if it wereFloat(or vice versa) when the type now propagates fromTableSchema. Silent coercion masks incorrect bound derivation.
Reuse upstream derived surface. If rai-rules-authoring or rai-graph-analysis ran earlier in the same session, the subtypes and properties they wrote back (classification subtypes, centrality scores, derived flags) are already in the model — they show up in inspect.schema(model). Reference them directly in solve_for / satisfy / minimize / maximize rather than re-deriving their logic in the formulation. See [examples/chainedrulesprescriptive.py](examples/chainedrulesprescriptive.py).
Confirm modeled dimensions. If the problem spans periods/scenarios/categories, that dimension must exist in the ontology — as a Concept (Week, Period, Scenario), as an Integer reference scoped via std.common.range(), or, for a small closed vocabulary, as a model.Enum (requires relationalai>=1.12; enum-indexed solve_for creates one binary per (entity, member) — see references/csp-formulation.md § Enum-indexed decision) — before Step 3. Constraints over the dimension are then a single declarative expression that quantifies automatically. Never plan to iterate over the dimension in Python or enumerate its values by hand; both bypass the engine. See rai-ontology-design if enrichment is needed.
When to skip: this step is cheap but not free. Skip on small greenfield models or one-shot formulations where the model fits in a single code block you just wrote.
Step 2: Define Variables
What decisions are being made? What can the solver control?
- Start with the base model context — examine concepts, properties, relationships
- Identify the primary decision entity first, then auxiliary/aggregation variables
- Choose variable types (continuous/integer/binary) and set bounds from data
- For minimize objectives, include a slack/unmet variable to avoid infeasibility
See [variable-formulation.md](references/variable-formulation.md) for context integration, variable principles, and advanced patterns (types, bounds, entity creation, slack, parametric/time-indexed).
Step 3: Define Constraints
What rules must the solution satisfy?
- Start with model structure + user goals
- For multi-period or recurrence patterns (inventory balance, sequencing, cumulative state), use declarative quantification:
Concept.ref()adjacency joins (w_prev.num == w.num - 1) orwhere=[t == std.common.range(start, stop)]— singlesatisfy()call grounds across all periods. See [constraint-formulation.md](references/constraint-formulation.md) > Temporal recurrence and [examples/multiperiodflowconservation.py](examples/multiperiodflowconservation.py). - Add forcing constraints first — these prevent trivial zero solutions for minimize objectives
- Add capacity/resource constraints from data properties
- Add flow conservation if the model has network structure
- Derive parameters from data ranges, not arbitrary values
Step 4: Define Objective(s)
What are we optimizing?
- Start with user's stated goal — map business language to minimize/maximize. See [objective-formulation.md](references/objective-formulation.md) for direction, multi-component objectives, and penalty terms
- Reference defined variables and data properties in the expression
- Check for trivial solution risk: "If all variables = 0, are all constraints satisfied?" If yes, Step 3 needs a forcing constraint.
- Competing objectives? If the formulation has a penalty term bundling two concerns (cost + penalty×slack), or a constraint that represents a competing goal (return ≥ threshold), consider whether the user wants to explore the tradeoff rather than fix a single point. See [multi-objective-formulation.md](references/multi-objective-formulation.md) for the epsilon constraint approach.
- Parameter sensitivity / what-if? If key constraints use fixed values that could vary (budget, demand, service level), use the Scenario Concept pattern — parameterize the constraint, index the decision variable by Scenario, and solve all scenarios in a single solve. This keeps results in the ontology and avoids manual re-solve loops. See [scenario-analysis.md](references/scenario-analysis.md).
Step 5: Validate
Is the formulation complete and correct?
- Every variable appears in at least one constraint or the objective
- Every constraint references at least one decision variable
- Join paths in
.where()clauses connect to actual data - Bounds are consistent (lower
subconcept for each decision variable). They appear ininspect.schema(model).concepts`:
from relationalai.semantics import inspect
schema = inspect.schema(model)
variables = [c for c in schema.concepts if "Variable" in c.extends]
constraints = [c for c in schema.concepts if c.name == "Constraint" or "Constraint" in c.extends]
objectives = [c for c in schema.concepts if c.name == "Objective" or "Objective" in c.extends]
# Confirm one Variable_ per solve_for call, one Constraint_ per satisfy,
# one Objective_ per minimize/maximize.
(b) Binding cardinality. Registration does NOT mean the variable binds to any rows. A solve_for(..., where=[always_false]) still registers a Variable_ subconcept but has zero bindings — the solver will run on an empty decision set. Check each Variable_ for non-empty binding:
for var_concept in variables:
resolved = model.concept_index[var_concept.name]
n = len(model.select(resolved).to_df())
if n == 0:
# The where= clause excluded every row. Fix the predicate
# (wrong property name, wrong threshold, missing join) and re-check.
raise ValueError(f"{var_concept.name} has 0 bindings")
(c) Coefficient presence. Properties referenced as objective or constraint coefficients must be populated by model.define(...) — an unbound coefficient Property has no tuples, so the join produces no row and the term doesn't appear in the formulation (PyRel relational semantics). The solver returns OPTIMAL with a vacuous objective. Distinct from (b): (b) catches empty decision-variable scope, (c) catches unbound coefficient data. Both surface as OPTIMAL with obj=0.
for coef_prop in objective_coefficient_properties:
n = len(model.select(coef_prop).to_df())
if n == 0:
raise ValueError(f"Coefficient {coef_prop} has no tuples — objective term won't ground")
(d) Constraint binding cardinality. Capture each satisfy() return value at declaration time, then verify the constraint grounded on the expected number of groupings before solving. Distinct from (b)/(c): (b) catches empty variable scope from a where= predicate, (c) catches unbound coefficients, (d) catches per-grouping bodies that didn't ground because a referenced bound was empty for some entities — under PyRel relational semantics an empty body produces no row, so the constraint applies to fewer entities than the .per(Entity) syntax suggests, and OPTIMAL returns with the entities that got no row effectively unconstrained.
cap_constr = problem.satisfy(model.require(usage Targeted Inspection.
**Simplify once correct:**
- Static parameters over dynamic calculations
- Objective terms for goals; constraints for hard requirements
- Group-level constraints over pairwise/granular combinations
Loop until the result is correct, fast enough, and defensible enough to take to Step 7. See [diagnostic-workflow.md](references/diagnostic-workflow.md) for failure-mode-by-failure-mode guidance and [formulation-simplification.md](references/formulation-simplification.md) for the simplification patterns in depth.
### Step 7: Present, React, Refine
Is the formulation complete — including constraints the user couldn't articulate upfront?
- Solve and present the result to the user (see Constraint Elicitation > Post-Solve: Iterative Refinement)
- Use the result as an elicitation tool to surface latent preferences
- Disambiguate rejections into constraint types, add them, re-solve
- Repeat until the user accepts or feasibility pressure forces prioritization
Steps 1-6 produce the best formulation you can build from what the user has told you. Step 7 discovers what they couldn't tell you until they saw a concrete result. Most real-world formulations require at least one pass through Step 7.
---
## Formul
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [RelationalAI](https://github.com/RelationalAI)
- **Source:** [RelationalAI/rai-agent-skills](https://github.com/RelationalAI/rai-agent-skills)
- **License:** Apache-2.0
- **Homepage:** https://relational.ai
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.