Skip to main content
Nori-Rel connects to linked database tables, builds features at each past date from what had happened by then, and has Nori predict what happens next. You describe the target in one line of PQL, and nothing is trained.
Nori-Rel connects to PostgreSQL, Snowflake, and Databricks. It needs Python 3.11, 3.12, or 3.13. Install nori-rel 0.4.3 or later with the extra for your database and the local extra, as each tab under Connect shows; see Installation. Local mode runs Nori in your process and downloads its weights from Hugging Face on first use. A GPU is faster but not required. The outputs on this page come from the public RelBench rel-f1 Formula 1 database.

Build a database from files

Skip this section if your data is already in related tables with declared keys. If your data is a folder of CSV or Excel exports, the nori-rel-prepare-data skill walks an AI agent through turning it into a PostgreSQL database Nori-Rel can use. The agent profiles the files, designs tables with declared keys, and leaves out columns that would leak the future, such as a customer’s lifetime total. Before it creates anything, it shows you every table, column, and relationship it plans and waits for your confirmation.
Then ask Claude Code to prepare your files for Nori-Rel. With another agent, give it the folder’s SKILL.md as its instructions.

Connect

Keep anything that identifies your account in environment variables. Nori-Rel reads them in your process and never sends them anywhere. Replace every value marked # yours with your own: the user, role, key path, host, warehouse, database, and schema shown are examples. Register the time column of each event table. Tables without one, such as drivers, are entities.
Connect with a role that can only read. Nori-Rel needs to read the schema and the tables you predict from, and nothing else. If you don’t know which column holds each table’s event time, connect once without time_columns and print every table’s columns with their database types:
With rel-f1 in PostgreSQL, this prints the following (three of the nine tables shown):
Pick the date or timestamp column that records when each row happened, add it to time_columns, and connect again. Here that is date in races and results. races.time is text, so it can’t be a time column. drivers.dob is a timestamp but it’s a date of birth, so drivers stays an entity. Type names follow your database.

See the graph

discover returns the tables, keys, and links Nori-Rel found. It reads the primary and foreign keys your database declares. Snowflake and Databricks record keys without enforcing them, and Nori-Rel checks them before it uses them. If your tables declare no keys, pass primary_keys= and foreign_keys= to connect. This helper turns the result into a Mermaid diagram:
Paste the output into any Mermaid renderer. For rel-f1 it draws nine tables: results, qualifying, and standings each link a driver to a race, and drivers has no time column because a driver is an entity rather than an event.

Set up the prediction

This example predicts each driver’s average finishing position over the next 60 days. In PQL that is one line. Four dates and one column set up the rest:
  • anchor_time is the date you predict from. Features for the drivers you score read only rows dated up to it.
  • anchor_times are past dates Nori learns from. At each one, Nori-Rel builds every driver’s features from rows dated up to that date and a label from the 60 days after it, so each date adds one example per driver. Each must be earlier than anchor_time.
  • observed_through is how far your data is complete. A past date is used only if its whole 60-day window ends by the earlier of anchor_time and observed_through, so a partly loaded month can’t produce wrong labels. The latest timestamp in the database can’t stand in for it, because a partly loaded period still has rows.
  • entity_created_at names the column in the entity table (here drivers) that says when each entity came into existence. A driver is left out of every past date before it, and a driver with no value is left out entirely. Most databases have a created_at column for this; rel-f1 has none, so the date of birth stands in.
Then predict. feature_strategy= picks how Nori-Rel builds the features, and on_event prints each stage as it happens. Pick a tab: each runs the same request. The rest of this page uses the NoriDFS result.
NoriDFS follows the foreign keys from the driver, including two hops away (the races and constructors behind each result), and fills a budget of 48 features. It takes the prediction date’s fields and the driver’s own columns first, then counts, recency, and ages of linked rows, then means of value columns until the budget runs out. It computes them in pandas, not SQL.
The 12 past dates cover the two years before July 2004. Every driver born by a date is a candidate example for it, 10,284 candidates in all. Only 190 of them raced in the 60 days after their date, so only those have a label, and Nori learns from those 190. max_context_rows=12_000 keeps every candidate in play; the default of 5,000 fills from the most recent dates and would drop the oldest ones. entity_created_at="dob" keeps each driver out of dates before their birth. The events show where the time goes. Reading the snapshot takes most of it, and that depends on your warehouse; planning features and running Nori take a few seconds. To run in the background, call client.start_predict(...) with the same arguments, then read run.events() or stop it with run.cancel().

Save the pipeline

details.pipeline records how the prediction was made: the task, the features NoriDFS planned, and the model settings. It holds no credentials, rows, or model weights. Save it to predict again later without a new feature search:

See the features

details.features is the matrix Nori scored: a pandas DataFrame with one row per driver, indexed by entity_id, and one column per feature. Each column name says what it reads: results.grid.mean is the mean of results.grid over the driver’s results rows, and qualifying.constructors.count counts the constructors reached through the driver’s qualifying rows. Transposed, each row is a feature and each column a driver:

Replay the pipeline

Load the saved pipeline, in this process or another one, and predict with it. It reads the database again and rebuilds the same features with the saved plan, with no feature search:
Replayed from the same date, it returns the same 857 predictions as the first run. Pass a later as_of to predict a new date with the same recipe. Each replay reads fresh rows from the database.

Evaluate the predictions

To check the pipeline, backtest it: replay it from past dates whose next 60 days are already in the database, then compare with what happened. This loop replays the saved pipeline at eight dates, two months apart, starting in July 2003. Each replay builds its context and features only from rows dated before its as_of, with the same features and settings every time. For a baseline, it uses each driver’s average finish over the year before the date, a simple “recent form” guess. The actual values come from a query on the same database. connection is any DB-API connection to it, such as psycopg.connect(...) or snowflake.connector.connect(...). Quote column names the way your database does.
Two of the eight dates fall in the off-season, with no race in the next 60 days, so 131 predictions across six dates have an actual value to compare with. Nori misses by 2.59 finishing places on average, against 2.76 for the recent-form baseline. That is a gap of 0.17 places, but on 131 predictions it is within noise: a bootstrap 95% interval for the gap runs from -0.08 to 0.43, and Nori is closer on 65 of the 131. Run the same comparison on your own data, with more dates, before you rely on a difference this small. The backtest gives the same errors as predicting each date from scratch with feature_strategy="noridfs", because NoriDFS plans the same features at each of these dates. A pipeline that holds an agent’s program replays that SQL unchanged, so backtesting it before the date of its search tests a program chosen with later data.

Good to know

  • Each prediction reads every table as of one moment. Snowflake uses Time Travel and Databricks pins Delta table versions, so those tables need history; views are not supported.
  • Local mode predicts numeric targets. A yes/no target such as COUNT(results.*,0,60,days)=0 needs hosted mode.
  • Your database bills the reads, including the key checks that run before extraction.

Next steps

Installation

The nori-rel extras for PostgreSQL, Snowflake, and Databricks.

Explainability

SHAP values, feature interactions, and partial dependence.