Skip to main content

Nori-Rel

Predict from relational databases with Nori. Nori-Rel reads a bounded database snapshot, constructs historical targets and FastDFS features, and runs local or hosted inference through one Python client.

The supported package covers PostgreSQL, Snowflake standard tables, and Databricks Unity Catalog Delta tables. Releases come from reviewed commits on main.

Review the support contract and dependency security assessment before deployment. The assessment includes scoped, expiring exceptions for upstream dependencies.

Install

Python 3.11, 3.12 and 3.13 are supported. Install your connector and, for the default local inference mode, the model runtime:

python -m pip install "nori-rel[database,local]"
# Other connectors:
python -m pip install "nori-rel[snowflake,local]"
python -m pip install "nori-rel[databricks,local]"

For hosted inference, omit local and set SYNTHEFY_NORI_API_KEY privately. A hosted installation does not require the local model runtime or model weights. See supported capabilities for platform and target limits.

Start from CSV or Excel files

If your data is a set of exported files rather than a database, the nori-rel-prepare-data skill walks an AI agent through converting them. The agent profiles the files and designs entity and event tables with declared keys, leaving out columns that would leak the future, such as a customer's lifetime total. It shows you every table, column and relationship it plans to create and waits for your confirmation before it touches the database. It then writes a load script you can rerun when new files arrive, and proves the schema with Nori-Rel's own discovery and one prediction.

With Claude Code, copy the folder into your skills, then ask for the conversion:

cp -r skills/nori-rel-prepare-data ~/.claude/skills/

With another agent, give it SKILL.md as its instructions.

Predict customer spend

This example expects existing users(user_id, created_at) and orders(order_id, user_id, event_time, amount) tables with declared primary and foreign keys. Set DATABASE_URL to a TLS-enabled PostgreSQL connection string. Use the synthetic example for a complete disposable dataset.

from datetime import UTC, datetime

from nori_rel import EnvironmentCredential, NoriRelClient

with NoriRelClient() as client:
    source = client.connect(
        engine="postgresql",
        credential=EnvironmentCredential(variable="DATABASE_URL"),
        tables=["users", "orders"],
        time_columns={"orders": "event_time"},
    )
    details = client.predict(
        source=source,
        query="PREDICT SUM(orders.amount,0,30,days) FOR EACH users.user_id",
        anchor_times=[datetime(2026, 1, 1, tzinfo=UTC)],
        anchor_time=datetime(2026, 3, 1, tzinfo=UTC),
        observed_through=datetime(2026, 3, 1, tzinfo=UTC),
        entity_created_at="created_at",
        return_details=True,
    )
    predictions = details.predictions
    print(predictions[["entity_id", "prediction"]])

The query predicts each user's total order amount over the next 30 days. anchor_times supplies historical examples; anchor_time is the scoring time. observed_through is your verified data-completeness watermark, not an estimate of completeness from the latest event. Use dates appropriate to your data and at least 32 usable historical rows. Register every event table's timestamp; other entity attributes must be immutable or valid at every requested anchor.

entity_created_at names a date or timestamp column on the entity table (users.created_at here) that holds when each user first existed. A user gets no training row for anchors before it and is not scored if created after anchor_time; a null drops the user. Nori-Rel does not create this column. If your table has none, add one, usually each entity's first event time. See PQL and time semantics for this column, rates written as COUNT(...)/COUNT(...), and the v0 limits, with worked examples.

The default mode runs Nori in your process. The first local prediction downloads model weights; subsequent predictions can use the populated cache offline. Use NoriRelClient(mode="hosted") to send the generated context/query feature matrices and labels to hosted Nori. Database connections, credentials, and raw source tables stay in the caller's process. Generated features can still contain sensitive information derived from those tables.

Each prediction chooses its feature search with predict(..., feature_strategy=...): "fastdfs" (the default), "noridfs", or an AgenticSearch. feature_strategy="noridfs" plans a NoriDFS relationship search over the foreign-key graph, with aggregation windows scaled to the target's label interval. It needs only the core dependencies.

details = client.predict(..., feature_strategy="noridfs")

start_predict takes feature_strategy too. NoriRelClient(features=...) is deprecated: it still sets the search for predictions that pass none, with a DeprecationWarning, and will be removed in 0.5.0.

AgenticSearch lets a language-model agent write SQL feature columns on top of NoriDFS. Install nori-rel[agentic]. Each candidate program runs locally in an in-memory DuckDB catalog and is scored with the client's own Nori predictor on held-out historical rows. A program is kept only if it also beats NoriDFS alone on a separate confirmation slice; otherwise the prediction uses NoriDFS alone.

from nori_rel import AgenticSearch

search = AgenticSearch.claude()  # reads ANTHROPIC_API_KEY
details = client.predict(..., feature_strategy=search)

Because the choice is per prediction, one client and one source can compare NoriDFS alone with an agent search, or agents on two models:

opus = AgenticSearch.claude(model="claude-opus-5-5")
sonnet = AgenticSearch.claude(model="claude-sonnet-5-5")
for strategy in ("noridfs", opus, sonnet):
    details = client.predict(..., feature_strategy=strategy)

AgenticSearch.openai(model=...) uses an OpenAI-compatible endpoint. The agent sends that model provider:

  • your schema: table and column names, row counts, keys, and time columns;
  • a profile of each column it asks about: the share of missing values, the number of distinct values, and either the 1st, 25th, 50th, 75th and 99th percentiles of a numeric column, the earliest and latest value of a date column, or the 10 most frequent values of any other column with their counts. Those values are raw cells, such as names, emails, or IDs;
  • up to 50 rows of each exploratory query result;
  • the error message of any query that fails, which can quote values.

Do not enable it for data that must not leave your environment for that provider. A search also costs provider tokens and extra Nori calls. Bound it with max_cost_usd (which also needs prices) or the search's limits. It is not exercised against a live model provider in release certification. See run control for stop and steer.

The agent keeps a program only if it lowers the error on held-out history. predict(..., metric=...) chooses that error for a regression target:

metric Error (lower is better)
"mae" (default) Mean absolute error
"mse" Mean squared error
"rmse" Root mean squared error
"nmae" MAE divided by the range (max minus min) of the actuals
"nrmse" RMSE divided by the range of the actuals
"medae" Median absolute error
"wape" Total absolute error divided by total actual
"mape" Mean absolute percentage error, over rows whose actual is not zero
"smape" Symmetric MAPE: mean of 2|error| / (|actual| + |forecast|)

On one holdout, NMAE and WAPE rank programs as MAE does and NRMSE as RMSE does: each divides by the same constant for every candidate. MSE and RMSE weigh large misses more. MAPE and SMAPE weigh every row equally in relative terms, so small series count as much as large ones. The metric changes which program is kept, not how the forecast is made. Trial and baseline events report it, with relative and normalized errors as percentages (Trial 3/24: WAPE 11.20% ...).

client.predict(source=source, query=query, feature_strategy=search, metric="wape", ...)

Inspect the features

With return_details=True, details.features is the feature matrix Nori scored: a pandas DataFrame with one row per entity_id and one column per feature. Each column name says what the feature reads: the tables it goes through from the entity, then the column, then the aggregate, joined with dots.

print(details.features.head())

With the tables above, orders.amount.mean is the mean of orders.amount over the user's orders and orders.count counts them; NoriDFS adds windows, such as orders.amount.mean_30d for the last 30 days. A feature two links away names both tables: with a products table that orders point to, orders.products.count counts the products reached through the user's orders. users.created_at is the user's own column, anchor.cutoff_month a field of the anchor time, and an agent's column is <query>.<column>. When two features would share a name, the table reached through a different foreign key shows that key, as in orders[buyer_id].count.

details.get_feature_table() returns one row per column of details.features, in the same order: its feature name, the internal_name the model, pipeline and feature hash use, its source, the sql that computed it, its dtype, and its missing_share, mean, min and max over the scored entities. sql is the agent's query for an agent column and the statements FastDFS ran for a FastDFS column; NoriDFS computes its features in pandas, so its sql is None.

table = details.get_feature_table()
print(table[["feature", "source", "missing_share", "mean"]])
print(table.set_index("feature").loc["orders.count", "sql"])

details.metadata["features"] is different: it names the feature search that ran ("fastdfs", "noridfs", "agentic" or "saved_program"), not the matrix.

Save and reload

Request prediction details to obtain a pipeline, then reconnect when using it:

from nori_rel import NoriRelPipeline

if details.pipeline is None:
    raise RuntimeError("This prediction cannot be exported as a pipeline")
details.pipeline.save("customer_spend.json")

pipeline = NoriRelPipeline.load("customer_spend.json")
with NoriRelClient() as client:
    source = client.connect(
        engine="postgresql",
        credential=EnvironmentCredential(variable="DATABASE_URL"),
        tables=["users", "orders"],
        time_columns={"orders": "event_time"},
    )
    predictions = client.predict(
        source=source,
        pipeline=pipeline,
        as_of=datetime(2026, 4, 1, tzinfo=UTC),
        observed_through=datetime(2026, 4, 1, tzinfo=UTC),
    )

A pipeline stores the task, feature recipe (a FastDFS or NoriDFS plan, plus the agent's SQL program after an agent search), schema contract, context policy, and model settings. Each replay reads fresh rows. It contains no credentials, source rows, or model weights. See pipeline export and the runnable save/reload example.

Operations and safety boundaries

Use the client as a context manager to close connection pools. PostgreSQL uses TLS and read-only transactions; warehouse connectors use pinned reads and require a dedicated identity without write privileges. Warehouse capabilities and snapshot guarantees are documented in the connector guide.

RelationalLimits bounds tables, rows, snapshot memory, feature size, and SQL statement time. Exceeding an extraction limit raises an error. These limits do not cap total warehouse billing or the process's total memory. See configuration and errors and progress/cancellation.

Development and licensing

See the public API inventory, CONTRIBUTING.md, SECURITY.md, and RELEASING.md. Public release changes are recorded in CHANGELOG.md.

Original Synthefy code is MIT-licensed. The vendored PQL grammar and derived parser retain Apache-2.0 licensing. Dependencies, downloaded model weights, and external datasets have their own terms; see ATTRIBUTION.md.

Metadata

Release files for nori-rel 0.4.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nori-rel 0.4.5
File Size Uploaded Provenance
nori_rel-0.4.5.tar.gz 451.2 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for nori-rel 0.4.5
File Interpreter ABI Platform
nori_rel-0.4.5-py3-none-any.whl Python 3 none any Details

Total release size: 738.4 kB

Release files / nori_rel-0.4.5.tar.gz

Download URL nori_rel-0.4.5.tar.gz
Size 451.2 kB
Tags Source
SHA-256 checksum
How to use checksums
b01ee939c7788141c03143547dd5b90c03a12998cc6e28bbb1e5b0f659f8bc16
BLAKE2b-256 checksum
How to use checksums
4f359bc19c8667815d7aef0343da86912b3fac2d303bccadd162d0100d946c55
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / nori_rel-0.4.5-py3-none-any.whl

Download URL nori_rel-0.4.5-py3-none-any.whl
Size 287.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
21573b43738b71eef2b175bc1bab404c2c3df6e5cc44c15da8f999d46d345fcd
BLAKE2b-256 checksum
How to use checksums
227632c96674ca62549d3c387ffbbcde70e0eb152e5e99d0472a0052f6e83cb8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Security for nori-rel 0.4.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for nori_rel-0.4.5.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page