AgentStack
SKILL verified Apache-2.0 Self-run

Rai Build Starter Ontology

skill-relationalai-rai-agent-skills-rai-build-starter-ontology · by RelationalAI

Walks through building a first RAI ontology from Snowflake tables or local data samples. Use when creating a new RAI model, starting a proof of concept, or onboarding a new dataset.

No reviews yet
0 installs
8 views
0.0% view→install

Install

$ agentstack add skill-relationalai-rai-agent-skills-rai-build-starter-ontology

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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 No

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.

Are you the author of Rai Build Starter Ontology? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Build Starter Ontology

Summary

What: Workflow for building a minimal working RAI ontology from raw data.

When to use:

  • Starting a new RAI project from Snowflake tables or local CSV files
  • User has raw data and wants a base model to query or build on
  • Bootstrapping an ontology before optimization or graph analysis

When NOT to use:

  • Enriching an existing model — see rai-ontology-design
  • Exported model — see rai-ontology-design
  • PyRel syntax — see rai-pyrel-coding
  • Query construction — see rai-querying

Overview:

  1. Scope — what questions must the model answer?
  2. Discover source data and run basic EDA
  3. Identify concepts (domain-first)
  4. Identify relationships and properties
  5. Validate design against schema
  6. Generate code
  7. Validate with queries, then emit inspect.schema() summary so the user sees what actually registered

Instructions

Required dependency: rai-ontology-design is the authority for all design decisions. This skill is the workflow. Use rai-pyrel-coding for syntax and [data-loading.md](../rai-pyrel-coding/references/data-loading.md) for data binding patterns.

Start from the business domain — what concepts exist, what questions must the model answer — then find data mappings. Domain-first modeling produces better models than table-to-concept mapping.

Write and maintain a document to help people onboard and understand the decisionmaking process. Document your findings and decisions for each step.

Interaction mode: Before starting, ask the user which mode they prefer:

  • Guided — present your proposed design at each step and confirm before proceeding. Best when the user has domain context or nuances to share along the way.
  • One-shot — produce the best result you can in a single pass. Best when the user wants speed and will review/iterate after.

The user knows their domain better than the data does — guided mode lets them steer you through subtleties that aren't obvious from the schema alone.

Step 1 — Scope

Before looking at any data, define:

  • 1-3 concrete questions the model must answer
  • What is out of scope — explicitly exclude tables/domains not needed yet

Keep the first version to ~10-15 must-have properties.

| Goal | In scope | Out of scope | |---|---|---| | Identify delayed orders | Orders, shipments, delay timestamps | Returns, carrier contracts, inventory |


Step 2 — Discover source data

Data loading decision: Use model.Table() for Snowflake-backed data (any size, production-ready). Use model.data() for prototyping with DataFrames only (≤ hundreds of rows) or inline scenario data not in Snowflake.

2a — Snowflake tables (recommended)

For connection setup, see rai-setup. Run discovery queries via the snow CLI or a Snowpark session:

from relationalai.config import SnowflakeConnection, create_config
from snowflake import snowpark

session: snowpark.Session = create_config().get_session(SnowflakeConnection)
session.sql("""
    SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE,
           NUMERIC_PRECISION, NUMERIC_SCALE
    FROM .INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_SCHEMA = ''
    ORDER BY TABLE_NAME, ORDINAL_POSITION
""").show()

> Keep this result available throughout the workflow. The DATA_TYPE and NUMERIC_SCALE columns are the authoritative source for deriving RAI property types in Step 6. When declaring each property, look up its source column in this result and use the type mapping table (Step 5) to pick the correct RAI type. Do not infer types from column names, CSV samples, or example ontologies.

2b — Local CSV / DataFrame (prototyping only)

model.data() is for prototyping only (hundreds of rows). For production, load into Snowflake and use model.Table().

Analysis

Identify data shape first. Source tables typically arrive in one of two shapes, and the distinction drives whether downstream steps derive metrics or bind them directly:

  • Raw events / measurements — one row per occurrence; metrics of interest must be derived in a downstream computed layer from the raw rows.
  • Pre-aggregated statistics — one row per entity, per pair, or per time bucket carrying already-computed values (including long-form (entity_i, entity_j, value) matrices). Metrics arrive ready-to-bind; do not re-aggregate already-aggregated values.

Two tables in the same schema can be different shapes — classify each independently. For long-form pairwise data specifically, see the pairwise value matrix example in [examples.md](references/examples.md).

Analyze source data per rai-ontology-design § Design Decision Sequence, step 1 (Analyze sources). Note:

  • _ID suffixes (likely PKs), columns matching other tables' PKs (likely FKs)
  • IS_/HAS_ prefixes (boolean flags), repeated string categories (enums)
  • Soft-joins (shared codes or categories across tables)

Basic EDA:

-- Row count and PK uniqueness
SELECT COUNT(*), COUNT(DISTINCT ) FROM ..;

-- FK cardinality
SELECT COUNT(DISTINCT ), COUNT(*) FROM ..;

-- Null rate
SELECT COUNT(*) AS total, COUNT() AS non_null,
    ROUND(1 - COUNT() / COUNT(*), 2) AS null_rate
FROM ..;

-- Value distribution for enum/category columns
SELECT , COUNT(*) AS cnt FROM ..
GROUP BY  ORDER BY cnt DESC LIMIT 20;

-- Subtype discovery: look for TYPE/CATEGORY/CLASS columns with few values
-- that partition entities into fundamentally different kinds
-- e.g., BUSINESS_TYPE with values 'Supplier','Customer' → subtypes of Business
SELECT , COUNT(*) AS cnt,
    ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS pct
FROM ..
GROUP BY  ORDER BY cnt DESC;

-- Relationship multiplicity: determine 1:1, 1:N, or M:N
-- Run for each FK to understand the join shape
SELECT
    ROUND(COUNT(*) * 1.0 / NULLIF(COUNT(DISTINCT ), 0), 2) AS avg_rows_per_fk,
    MAX(cnt) AS max_rows_per_fk
FROM (
    SELECT , COUNT(*) AS cnt
    FROM ..
    GROUP BY 
);

For graph/network models — trace the topology before modeling:

When your data describes a network (nodes connected by edges via FK chains), trace the full path before Step 3:

-- What node types connect to what? Reveals tiers and layers.
SELECT source.TYPE AS from_type, dest.TYPE AS to_type, COUNT(*) AS edges
FROM  e
JOIN  source ON e.SOURCE_ID = source.ID
JOIN  dest ON e.DEST_ID = dest.ID
GROUP BY from_type, to_type ORDER BY edges DESC;

-- Check for NULL FKs that create invisible network gaps
SELECT COUNT(*) AS broken_edges FROM  WHERE DEST_ID IS NULL OR SOURCE_ID IS NULL;

This reveals hidden layers, missing connections, and whether you need intermediate concepts to bridge tiers.


Step 3 — Identify concepts

Work domain-first: brainstorm business entities, then map to tables. Each concept must map to an authoritative source with a clear identity. Refer to rai-ontology-design § Concept Design Principles.

Check for subtypes: If Step 2 EDA found TYPE/CATEGORY columns that partition a concept into fundamentally different kinds (e.g., a BUSINESS_TYPE column with values Supplier and Customer), model these as subtypes using extends. Use subtypes when each kind carries its own properties or relationships — not for every enum column.

Validate:

  • Each concept maps to at least one authoritative source
  • Each concept has a clear identity
  • Concepts are orthogonal (no two with identical entity sets)

Identity hygiene. Prefer minimal identity (1-2 natural-key fields). Oversized identity is a signal: usually it means a surrogate ID was missed, or the entity is really a measurement that belongs as a Relationship payload on a smaller concept — both cases inflate the model and complicate downstream queries. If the only natural identity you can find is >3 fields, pause and ask: (a) does a surrogate ID exist in the source table that you missed?, (b) is this really a domain concept, or a measurement / derived row that belongs as a Relationship payload on a smaller concept? When the answer to both is no — keep the compound identity, but flag the concept as a candidate for scope cuts in Step 1.


Step 4 — Identify relationships and properties

Choose Property or Relationship per link based on the Step 2 multiplicity results:

  • Property — when each input uniquely determines one output. Covers scalar attributes AND functional FKs (e.g., each Order has exactly one Customer → Order.customer = model.Property(f"{Order} placed by {Customer:customer}")).
  • Relationship — when one input can have many outputs (one Customer → many Orders), the association has multiple fields, or it's a Boolean flag ({Entity} is , unary).

Anti-bundle rule. Each scalar attribute should be its own Property. If you find yourself packing more than two unrelated scalars into a single Relationship, split them into individual Property declarations. Bundles become un-queryable: you can't filter or aggregate on a single field. Reserve multi-field Relationships for genuinely co-occurring data (e.g., a date range {Promotion} active from {DateTime:start} to {DateTime:end}).

Same-type-slot disambiguation. When a Relationship has two slots of the same concept type (e.g., {Location} ... {Location}), the slots are roles and must be distinguished. Either (a) split into two Property declarations with role short_names, or (b) label both slots: f"{Lane} from {Location:origin} to {Location:destination}". Without this, model.define(...) will silently bind both slots to whichever column you list first, collapsing source and destination into the same entity. Verify with a Step 7c query that origin ≠ destination on at least one row.

See rai-ontology-design § Property vs. Relationship and rai-pyrel-coding § Properties and Relationships for the full decision rule.


Step 5 — Validate design

Validate the proposed design against source data before coding:

| Check | What to confirm | |-------|-----------------| | Identity columns | Exist in source and uniquely identify rows | | Associations (Property/Relationship) | Valid FK or join key exists, multiplicity determined (1:1 / N:1 → Property; 1:N / M:N → Relationship) | | Properties | Source column exists with compatible data type | | Concept grounding | Every concept maps to an authoritative source | | Orthogonality | No two concepts represent the same entity set |

Type validation against Snowflake schema

Verify that every property's RAI type matches its Snowflake source column. Use this mapping:

| Snowflake type | RAI type | Notes | |---|---|---| | VARCHAR, TEXT, STRING | String | | | NUMBER with scale > 0 (e.g., NUMBER(18,2)) | Float | Check NUMERIC_SCALE in INFORMATIONSCHEMA | | NUMBER, INT, INTEGER (scale = 0) | Integer | | | FLOAT, DOUBLE, REAL | Float | | | DATE | Date | | | TIMESTAMPNTZ, TIMESTAMP_LTZ, TIMESTAMP | DateTime | | | BOOLEAN | Boolean property, or unary Relationship for flag-style patterns | |

Run this query to pull actual column types for comparison:

SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, NUMERIC_PRECISION, NUMERIC_SCALE
FROM .INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ''
  AND TABLE_NAME IN ('', '')
ORDER BY TABLE_NAME, ORDINAL_POSITION;

Always run this query before writing property declarations. A single type mismatch between any RAI property and its Snowflake column causes a TyperError that blocks ALL queries on the entire model — not just the mismatched property — with no indication of which property failed.

Common mismatches to check:

  • DATE vs TEXT — Columns with date-like names (signup_date, created_at) may be stored as TEXT in Snowflake, especially when loaded from CSV. Check the actual type; use String if the column is TEXT.
  • DATE vs TIMESTAMP_NTZ — Columns with date-like names (ship_date, scheduled_arrival_date) may be stored as TIMESTAMP_NTZ (or _LTZ/_TZ), not DATE. Use DateTime, not Date, when INFORMATION_SCHEMA.COLUMNS.DATA_TYPE returns a TIMESTAMP variant. A Date declaration against a TIMESTAMP_NTZ column raises TyperError at first query, blocking ALL queries on the model.
  • NUMBER precision — A column typed NUMBER(38,0) maps to Integer, but NUMBER(18,4) maps to Float. Always check NUMERIC_SCALE; if scale > 0, use Float. If a NUMBER(38,0) column holds values that are conceptually continuous (e.g., CAPACITY_MW, COST_MILLION), flag this to the user — the DDL may need FLOAT or NUMBER(18,2) instead.
  • BOOLEAN — Model as Boolean property or unary Relationship(f"... is "). Both work; prefer Relationship when the flag semantics read naturally (e.g., "Order is urgent").
  • Numeric IDs — A column named CREDIT_CARD_NUMBER or PHONE may be NUMBER, not TEXT. Let the schema dictate the type, not the name.

Step 6 — Generate code

> GATE: Do not proceed until Step 5 type validation is complete.

Use the type mapping table and INFORMATION_SCHEMA.COLUMNS output from Step 5 to derive every property type.

Follow conventions in rai-pyrel-coding and rai-ontology-design. Put everything in a single file named after the domain:

.py     # e.g., supply_chain.py, fraud.py, inventory.py

A single file is easiest to iterate on. When the model grows (multiple reasoners, derived layers, shared computed logic), split into a package — see rai-ontology-design § Layering Principles.

> If you do split into a package: Never name the directory model/ if your Model variable is also called model. Python resolves import model.xxx to the directory, shadowing the variable. Use a domain-specific name (e.g., sc_model/, fraud_model/).


Step 7 — Validate with queries

Add validation queries to the bottom of your .py file to confirm data loaded correctly before answering scoped questions. Fix any import errors or empty results before considering the starter ontology complete. For query syntax, see rai-querying.

> Note: Import aggregates with from relationalai.semantics.std import aggregates.

7a — Spot-check property types against schema before running any queries. For each BOOLEAN, DATE, and NUMBER column in the Step 2 schema output, confirm the corresponding property declaration uses the correct RAI type. These three are the most common sources of silent type mismatches (TyperError at query time with no indication of which property failed). Fix any mismatch before proceeding.

7b — Count instances per concept to confirm data binding loaded rows:

from relationalai.semantics.std import aggregates

df = model.select(aggregates.count(MyConcept).alias("count")).to_df()
print(df)
# Expect: count matches source table row count. Zero means data binding failed.

> Note: aggregates.count(C) on a concept with zero instances returns an empty DataFrame (no rows), not a DataFrame with count=0 — the underlying relation is empty so the aggregation has no rows to reduce over. For chained workflows where a placeholder concept is populated by a downstream step, use inspect.schema() membership to verify the concept is declared rather than count() to verify data.

7c — Verify relationships to confirm FK joins resolved:

df = model.select(
    ConceptA.id.alias("a_id"),
    ConceptB.id.alias("b_id"),
).where(
    ConceptA.my_relationship(ConceptB)
).to_df()
print(f"Linked pairs: {len(df)}")
# Expect: non-empty results. Empty means the FK join didn't match.

7d — Answer scoped questions from Step 1:

df = model.select(
    MyConcept.id.alias("id"),
    MyConcept.some_property.alias("value"),
).where(
    MyConcept.some_property > threshold
).to_df()
print(df)
# Each scoped question from Step 1 should be answerable with a query like this.

7e — Report what actually registered. After the model runs cleanly, emit an inspect.schema() summary for the user. This is the canonical "here's what got built"

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.