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.
Install
Section titled “Install”macOS and Linux
Section titled “macOS and Linux”curl -fsSL https://get.deql.osm-data.com | shThe 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.
Windows
Section titled “Windows”Currently only supported via WSL using the Linux install.
Give Decisionhouse two pieces of DeQL
Section titled “Give Decisionhouse two pieces of DeQL”Decisionhouse takes two pieces of DeQL and gives back a table:
- candidate definitions, one or more
CREATE CANDIDATESstatements that say what may be decided, one row per decision; - a query, one
DECIDEstatement that says what to choose: the decision columns, the constraints they must satisfy, and the objective.
deql candidates.deql query.deqlEach 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.
Register data
Section titled “Register data”The CLI has no data of its own. The quickest source is a file:
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,value1,5,44.612,20,22.133,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.
Create candidates
Section titled “Create candidates”A candidate set is the space of things that can be decided. One row is one decision.
CREATE CANDIDATES itemsDECISION KEY (id)AS ( SELECT * FROM inventory);itemsis the name theDECIDEstatement will select from. It is not the source table; it is the new candidate set built from it.DECISION KEYnames the column or columns that identify a row uniquely. It is required, must be non-empty, and every column must exist in theSELECToutput.- 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_assignmentsDECISION 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.
Describe the decision
Section titled “Describe the decision”DECIDE INTO knapsackFROM itemsDECISION COLUMNS ( is_selected INTEGER BETWEEN 0 AND 1)SUBJECT TO CONSTRAINT budget: SUM(weight * is_selected) <= 120MAXIMIZE SUM(value * is_selected);DECIDE INTO knapsacknames the result.FROM itemsis the candidate set, never a raw table.DECISION COLUMNSdeclares the unknowns. Each one becomes a new column in the output, holding the value the solver picked for that row.SUBJECT TOlists the constraints, comma-separated.MAXIMIZEorMINIMIZEgives 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.
Run it and read the result
Section titled “Run it and read the result”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 · highsThe 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.
Export
Section titled “Export”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_selected1,5,44.61,0.02,20,22.13,0.03,12,98.8,1.0Serve it instead: deql server
Section titled “Serve it instead: deql server”deql server keeps the same engine alive behind
Arrow Flight SQL, so
statements arrive from clients and results go back as Arrow:
deql server --register-files inventory.csv --candidates candidates.deqlDecisionhouse server listening on 127.0.0.1:6134The 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.
Connect with Arrow Flight SQL
Section titled “Connect with Arrow Flight SQL”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"].
Use MCP
Section titled “Use MCP”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 example
Section titled “Assignment example”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:
CREATE CANDIDATES pool_assignmentsDECISION 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);DECIDE INTO allocationFROM pool_assignmentsDECISION 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);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 · highsOne 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.