Skip to content
Limited preview Core mixed-integer linear modeling and execution are available. Formulation planning and algorithmic discovery are not included yet.

Quickstart

Decisionhouse is a Rust-based optimization query engine that runs where the data already is. DeQL, the Decision Query Language, extends SQL with statements for defining candidate sets and expressing optimization problems. The usual way to solve a problem like allocation or scheduling is to pull a table into Python, build the model variable by variable against a solver API, solve it, and join the answer back. Decisionhouse replaces that round trip: describe the decision in DeQL and get back the rows you started from, with the solver’s choice added as columns.

Just want to try Decisionhouse with an agent? Jump straight to the MCP server guide.

This preview models mixed-integer linear programs. Pick a subset under a budget, assign work to machines, allocate a scarce resource, choose a portfolio; the problems are usually handled by hand-written solver glue wrapped around a query. Decisionhouse reads the data in place (CSV, Parquet, Postgres, DuckDB, ClickHouse) and compiles the statement into the model itself, variables, constraints, and coefficients, all built by the same query engine that reads the rows.

What you get here is a limited preview. This means features like formulation planning and algorithmic discovery, which our VLDB paper covers, are not included in this preview yet but will come soon. For more background and the motivation, we highly recommend reading our VLDB26 paper: Decisionhouse: Prescriptive Analytics in the Data Stack.

Terminal window
curl -fsSL https://get.deql.osm-data.com | sh

The installer asks where the data is, a database URL or a file path, then writes deql/deql-config.toml, places the binary beside it, and leaves you in a shell inside ./deql. Everything it sets is a default you can edit afterwards, the server address 127.0.0.1:6134 included. The binary is not on $PATH: it is ./deql in that folder, which the rest of this guide writes simply as deql. If you want the CLI convenience, the installation guide shows how to add that directory to PATH without moving or copying the binary.

Currently only supported via WSL using the Linux install.

Decisionhouse takes two pieces of DeQL and gives back a table:

  • candidate definitions, one or more CREATE CANDIDATES statements that say what may be decided, one row per decision;
  • a query, one DECIDE statement that says what to choose: the decision columns, the constraints they must satisfy, and the objective.
Terminal window
deql candidates.deql query.deql

Each argument is either a path or the SQL itself, so deql candidates.deql "DECIDE INTO ..." works as well; --candidates and --query are the flag equivalents.

The same engine also runs as a server, taking those statements from clients over Arrow Flight SQL.

The CLI has no data of its own. The quickest source is a file:

Terminal window
deql --register-files inventory.csv candidates.deql query.deql

--register-files takes a comma-separated list of CSV or Parquet files and registers each one as a table named after its file stem. inventory.csv becomes the table inventory:

id,weight,value
1,5,44.61
2,20,22.13
3,12,98.80
...

The alternatives are --connection <url> for an external database, or a deql-config.toml listing tables and databases. DeQL automatically finds that file in the working directory or beside the installed binary. An explicit --config <path> overrides both locations. With no config, --connection, or --register-files, it asks where the data is and writes a config. deql init starts that setup directly.

A candidate set is the space of things that can be decided. One row is one decision.

CREATE CANDIDATES items
DECISION KEY (id)
AS (
SELECT *
FROM inventory
);
  • items is the name the DECIDE statement will select from. It is not the source table; it is the new candidate set built from it.
  • DECISION KEY names the column or columns that identify a row uniquely. It is required, must be non-empty, and every column must exist in the SELECT output.
  • The body is ordinary SQL over the registered tables: joins, filters, computed columns, VALUES, whatever produces one row per candidate. It is evaluated and cached once, when the candidate definition runs.

Candidate rows do not have to exist in the data. Assignment problems build them with a join, so that one row is one pairing:

CREATE CANDIDATES pool_assignments
DECISION KEY (pool_id, workload_id)
AS (
SELECT pool_id, workload_id, capacity, demand, mem_gb, min_mem, cost
FROM gpu_pools
CROSS JOIN workloads
WHERE mem_gb >= min_mem
);

A candidates file may hold several statements separated by ;. Candidate sets live only for the length of one process, so pass the candidate definitions on every run.

DECIDE INTO knapsack
FROM items
DECISION COLUMNS (
is_selected INTEGER BETWEEN 0 AND 1
)
SUBJECT TO
CONSTRAINT budget: SUM(weight * is_selected) <= 120
MAXIMIZE SUM(value * is_selected);
  • DECIDE INTO knapsack names the result.
  • FROM items is the candidate set, never a raw table.
  • DECISION COLUMNS declares the unknowns. Each one becomes a new column in the output, holding the value the solver picked for that row.
  • SUBJECT TO lists the constraints, comma-separated.
  • MAXIMIZE or MINIMIZE gives the single objective.

Inside constraints and the objective, a bare identifier is either a decision column or a column of the candidate set. SUM(...) aggregates over the candidate rows; a constraint written without SUM applies to each row on its own.

A decision column may not reuse the name of a candidate column.

The senses are <=, >= and =. The right-hand side may be a literal or a candidate column.

MAXIMIZE or MINIMIZE one linear expression. Sums may be added and subtracted and a constant term is allowed: MAXIMIZE SUM(value * pick) - SUM(DISTINCT fixed_cost * open). SUM(DISTINCT ...) counts a variable declared with BY once per group rather than once per row.

Terminal window
deql --register-files inventory.csv candidates.deql query.deql
+-----+--------+--------+-------------+
| id | weight | value | is_selected |
+-----+--------+--------+-------------+
| 1 | 5 | 44.61 | 0.0 |
| 2 | 20 | 22.13 | 0.0 |
| 3 | 12 | 98.8 | 1.0 |
| 4 | 20 | 79.93 | 0.0 |
| 5 | 2 | 66.87 | 1.0 |
| 6 | 4 | 35.45 | 1.0 |
| 7 | 18 | 7.0 | 0.0 |
| 8 | 4 | 85.93 | 1.0 |
| 9 | 1 | 93.58 | 1.0 |
| 10 | 14 | 25.71 | 0.0 |
...
| 100 | 8 | 48.9 | 0.0 |
+-----+--------+--------+-------------+
optimal · objective 2029.52 · highs

The table is the candidate set with the decision columns appended. Every candidate row comes back, all 100 here, in candidate-set column order, with one extra column per decision column. Rows that were not chosen are present with a value of 0.0, so filtering the output is your side of the job: the 31 rows above with is_selected = 1.0 weigh 120 in total, exactly the budget.

Decision values are printed as floats even for INTEGER and BINARY columns, because they come back from the solver as numbers. A column declared BY (...) repeats its group’s single value on every row of that group.

The last line is the solver’s report, three fields separated by ·. Infeasible and unbounded problems produce no table at all; they exit non-zero with an error.

Terminal window
deql --register-files inventory.csv candidates.deql query.deql \
--export solution.csv

--export writes the same rows to CSV in addition to printing them, one header line for the whole file:

id,weight,value,is_selected
1,5,44.61,0.0
2,20,22.13,0.0
3,12,98.8,1.0

deql server keeps the same engine alive behind Arrow Flight SQL, so statements arrive from clients and results go back as Arrow:

Terminal window
deql server --register-files inventory.csv --candidates candidates.deql
Decisionhouse server listening on 127.0.0.1:6134

The data options are the same, --config, --connection, --register-files, --candidates, but server is a subcommand, so there is no positional query; it comes from a client. The address is [server] in the config, or HOST/PORT, defaulting to 127.0.0.1:6134. There is no authentication and no TLS.

Use any Arrow Flight SQL client. From Python, after pip install adbc-driver-flightsql pyarrow:

import adbc_driver_flightsql.dbapi as flight_sql
cursor = flight_sql.connect("grpc://localhost:6134").cursor()
cursor.execute(
"CREATE OR REPLACE CANDIDATES items "
"DECISION KEY (id) AS (SELECT * FROM inventory)"
)
cursor.execute(open("query.deql").read())
solution = cursor.fetch_arrow_table()

One connection carries CREATE CANDIDATES, which answers OK, DECIDE, and ordinary SQL. Candidate sets belong to the server process and are shared by every client, so define them once with --candidates and let clients send only DECIDE. GetCatalogs, GetDbSchemas and GetTables are implemented, so a client can list what is registered without being told.

Two things differ from the printed table. 0/1 decision columns act as a filter rather than as output: only rows where every one of them was chosen come back, and those columns are dropped. The knapsack returns 31 rows of id, weight, value, not 100 rows with is_selected. Columns with any other bounds are returned as-is and filter nothing.

The solver’s report travels as JSON in the Arrow schema metadata:

json.loads(solution.schema.metadata[b"deql.solution_info.v1"])
# {
# "objective_value": 2029.52,
# "objective_sense": "maximize",
# "status": "optimal",
# "solver": "highs",
# "algorithm": "mixed_integer_programming",
# "warm_start": "not_attempted"
# }

A client already holding Arrow can skip CREATE CANDIDATES and push rows in over Flight SQL’s bulk ingest, naming the decision key columns in the deql.candidate_keys option as a JSON array, ["id"].

deql mcp is a smaller, local server for trusted agents. It speaks MCP over stdio, takes the same --config, --connection, --register-files and --candidates data options, and exposes catalog, query, create_candidates, decide and drop_candidate as tools. Results are bounded, 100 rows by default, 1,000 at most, capped at 1 MiB of tool output, so Flight SQL stays the interface for complete or high-volume Arrow.

Point your MCP host at the installed binary and pass only mcp. The installer places deql-config.toml beside the binary, and deql mcp discovers it automatically. See the MCP server guide for a complete configuration, setup commands for Codex and Claude Code, and custom options. No source checkout is needed.

Assignment, from a cross join, with grouped constraints on both sides. pool_id and workload_id together key the candidate set, gpus is how many GPUs a pool gives a workload, and each constraint is repeated per group by BY:

pools_candidates.deql
CREATE CANDIDATES pool_assignments
DECISION KEY (pool_id, workload_id)
AS (
SELECT pool_id, workload_id, capacity, demand, mem_gb, min_mem, cost
FROM gpu_pools
CROSS JOIN workloads
WHERE mem_gb >= min_mem
);
pools_query.deql
DECIDE INTO allocation
FROM pool_assignments
DECISION COLUMNS (
gpus INTEGER BETWEEN 0 AND 6
)
SUBJECT TO
CONSTRAINT capacity: SUM(gpus) <= capacity BY (pool_id),
CONSTRAINT demand: SUM(gpus) = demand BY (workload_id)
MINIMIZE SUM(cost * gpus);
Terminal window
deql --register-files gpu_pools.csv,workloads.csv \
pools_candidates.deql pools_query.deql
+---------+-------------+----------+--------+--------+---------+------+------+
| pool_id | workload_id | capacity | demand | mem_gb | min_mem | cost | gpus |
+---------+-------------+----------+--------+--------+---------+------+------+
| 1 | 101 | 8 | 4 | 24.0 | 8.0 | 255 | 0.0 |
| 1 | 102 | 8 | 1 | 24.0 | 8.0 | 255 | 0.0 |
...
| 40 | 226 | 4 | 5 | 32.0 | 32.0 | 218 | 1.0 |
+---------+-------------+----------+--------+--------+---------+------+------+
optimal · objective 137891 · highs

One row per surviving pool/workload pair, so the output is as large as the cross join. The interesting rows are the ones with gpus > 0.