
Rudder Data Graphs
- 1 installs
- 18 repo stars
- Updated July 17, 2026
- rudderlabs/rudder-agent-skills
Produces RudderStack Data Graph YAML from RETL sources by categorizing sources into entities, events, and audiences for Audiences.
About
Analyzes a customer's RETL sources and warehouse schema to design a Data Graph YAML for RudderStack Audiences, resolving warehouse account IDs and join keys. A developer or solutions engineer uses it when mapping RETL data into entity/event graphs and identifying untapped segments.
- Categorizes RETL sources into entity/event/audience/supporting anchored on audiences
- Resolves warehouse account_id via rudder-cli workspace accounts list and verifies join keys
Rudder Data Graphs by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,803 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Jul 18, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rudderlabs/rudder-agent-skills --skill rudder-data-graphsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 18 |
| Last updated | July 17, 2026 |
| Repository | rudderlabs/rudder-agent-skills ↗ |
What it does
Produces RudderStack Data Graph YAML from RETL sources by categorizing sources into entities, events, and audiences for Audiences.
Files
RETL Connection Analysis & Data Graph Design
What this skill produces
Given a customer name (or workspace / org identifier), this skill produces five deliverables, in order:
1. Source Inventory — every relevant RETL source categorized as entity / event / audience / supporting-only. 2. Untapped Segment List — realistic, filterable business dimensions the customer has not yet expressed as audiences. 3. Data Graph YAML — one per workspace / domain, ready for validation in rudder-cli or the visual builder. 4. Demo Warehouse Spec — tables, key columns, joins, and known / inferred schema details needed to mock the graph. 5. Action Items — priority-ordered next steps, including the upgrade narrative and any assumptions to confirm.
The end-to-end flow is: find workspaces → list RETL sources → shortlist relevant sources → fetch full configs for the shortlist → categorize → design graph → validate → hand off. Each step below says what to do and why, so you can adapt when the customer doesn't match the common shapes.
---
Step 1 — Find the customer's workspaces
1. admin_search_organizations(company_name=...) to get the org_id. 2. admin_search_workspaces(search_by=organizationId) using that id. 3. Keep only status=ACTIVE workspaces. Why: inactive workspaces often have stale or disconnected configs and will produce misleading source lists. 4. Note the likely warehouse-backed workspaces, but do not assume you already have the correct `accountId` at this stage. Why: the Data Graph YAML embeds account_id, and that value is the warehouse account the graph runs against. The most authoritative source for it is rudder-cli workspace accounts list --json (see Step 4a); a RETL source config is a convenient secondary hint when one already exists, but it is not a prerequisite.
---
Step 2 — Discover sources, anchored on audiences
Run list_sources(workspace_id=..., retl_type=all, includeConfig=false) per workspace. Why `includeConfig=false`: full configs blow the context budget on large workspaces; start broad, then fetch detail only for sources you actually need.
Important: includeConfig=false is a discovery pass only. You cannot extract filterSpec, SQL, join hints, or reliable accountId values from that response alone. After shortlisting sources, fetch full config for the shortlisted source ids before making graph decisions.
Audience sources are the anchor signal. Why: this skill exists to build Audiences. Audience RETL sources are the customer's own pre-validated statements about what they segment on. A workspace can have hundreds of table sources but only a handful of audiences, and the audiences point directly at the entities that matter for activation.
Discovery order:
1. List every audience source first from the discovery pass. 2. Fetch full config for those audience sources and extract:
config.filterSpec.filterGroups[].filters[].fieldName— the fields the customer actually filters on.- The underlying table/model reference — this is the anchor entity for that audience.
primaryKeyand any source-levelaccountIdif present.
3. Pull in the tables/models that back those audiences. Fetch full config for those backing sources too. These are non-negotiable: you need their schema shape to size the entity properly and to compute untapped segments in Step 6. 4. Scan remaining tables/models for supporting roles:
- Event candidates — tables/models with a real business-time timestamp (
ordered_at,sent_at,opened_at, etc.) and a clear relationship back to an entity. Pull these in; events are a major Data Graph unlock that audiences alone can't surface. - FK targets — tables/models referenced by foreign keys from anchor entities (e.g., if the anchor
contacthas abranch_id, pull inbranch). Pull these in. - Everything else — keep in the Source Inventory for completeness, but do not force into the Data Graph unless it materially improves segmentation or the customer asks. Noise suppression matters on large workspaces.
5. Resolve `accountId` only after the shortlist exists. Cross-check the warehouse account id from the specific RETL source configs you are actually using in the graph against rudder-cli workspace accounts list (see Step 4a). If shortlisted sources disagree on accountId, stop and flag it; one graph should not span multiple warehouse accounts.
What to extract, by sourceType:
sourceType | Fields to read | What you can trust from it |
|---|---|---|
audience | config.filterSpec.filterGroups[].filters[].fieldName, underlying table/model ref, primaryKey, accountId if present | fields already used for segmentation, anchor entity, graph account hint |
table | config.table, config.schema, primaryKey, accountId if present | physical table identity and PK |
model | config.sql, primaryKey, accountId if present | model intent and projected columns/aliases; not guaranteed types |
Fallback — customer has no audience sources yet
This is common. RETL Audiences is recent; many customers have extensive table/model sources but zero audiences. Don't stop — this is the exact customer who most needs the Data Graph pitch.
When audiences are absent, switch the anchor:
1. Ask the customer (or infer from destination mappings) which entities they currently activate on. If dim_contact is synced to HubSpot, contacts are an activation entity. Treat those tables/models as anchor entities. 2. Apply the same "pull in FK targets and event candidates" rules around those anchors. 3. Frame Step 6 differently: this is not "unused audience fields" anymore. It becomes "high-value segment ideas available from the entity's business dimensions and related events." 4. State this explicitly in Step 7: the customer is being introduced to the feature, not shown gaps in an already-mature audience program.
---
Step 3 — Categorize into entity / event / audience
Every shortlisted source becomes one of:
- Entity — stable single-row business object with a durable primary key. Examples: users, branches, contacts, accounts, products.
- Event — row semantics are "something happened at time X" and the row has a true business timestamp. Examples: orders, leads sent, page views.
- Audience overlay — an audience source is not a new entity in the Data Graph; it is a saved filter over an existing entity or model.
- Supporting-only — useful for analysis or demo context but not worth modeling directly in the graph.
Edge cases (these come up often enough to call out):
- Composite primary key — do not casually pick one component and do not silently concatenate unless that synthetic id is stable, documented, and actually unique. Prefer an upstream model that exposes a deliberate single-column id for the graph.
- No primary key — usually an event, aggregate, or throwaway helper table. Confirm before modeling it as an entity.
- SCD2 / row-versioned dimensions — if a dimension carries a surrogate row-version key (
*_KEY), a stable business id, and effective/end-date columns, use the *surrogate `_KEYasprimary_id** — that is the column fact tables join on; the business id is usually absent from facts. The graph includes every version row unless you filter to current rows (is_current = true`) at the model layer, so unfiltered SCD2 entities over-count. - Templated SQL in model sources — render it with the current context before parsing columns; raw template text will miss projected fields.
- Ingestion-metadata timestamps (`snapshot_date` / `loaded_at` / `_etl_timestamp` / `created_at`) — not true event timestamps. They record when the row was written, not when the business event happened. Do not classify the source as an event unless a real business-time column exists — and when one does (e.g.
event_at,ordered_at), use it for the event'stimestamp, never the ingestion column. - Soft-deleted rows — if the table has an
is_deleted/deleted_atcolumn, the Data Graph will include them unless filtered. Note this; the customer may want a filter at the model layer. - Multi-tenant columns — if the warehouse is multi-tenant (e.g.,
tenant_id), surface that column in the entity shape and call out scoping expectations. - Audience wraps a model that joins multiple tables — the model is the entity shape; the audience is just a saved filter on top. Use the model for the Data Graph node, not the base tables unless you are intentionally decomposing the model.
---
Step 4 — Design the Data Graph
One Data Graph per workspace / domain / warehouse account. Why: cross-workspace joins are not supported, and mixing domains (e.g., Consumer + B2B) in one graph muddies the builder UX. Also, all models in a graph must resolve to the same warehouse account.
Entity selection:
- Root entity — the primary "who" being activated (users, branches, contacts). Mark it in YAML with
root: trueon the entity model (entity-only field). Multiple roots are valid — eachroot: trueentity is an independent audience-builder anchor, so a graph may have one or several. The validator does not enforce a count (zero, one, or many all pass), so setrootdeliberately on whichever entities the builder should start from rather than relying on a default. - Related entities — supporting objects with foreign-key relationships to the root.
- Events — timestamped activity tables tied back to an entity.
Relationship direction — common footgun:
source_join_keyis the column on the current model (the one declaring the relationship).target_join_keyis the column on the target model.- Cardinality describes how many target rows exist per source row:
branch → contactsisone-to-manyfrom the branch's perspective. - Declare each relationship once, on one side only. Do not also declare the inverse on the target model — the graph traverses it both ways. A double-declaration collides on
display_name(which must be unique across all relationships) and inflates the relationship count.
Verify join keys before writing the YAML. A wrong join key passes validate but silently returns the wrong audience population. If you have direct warehouse access (an MCP server, a CLI like snowsql/bq/psql, or an IDE connection), confirm every source_join_key and target_join_key exists on its table with one batched introspection query — RETL config alone does not catch typos or renamed columns:
| Warehouse | One-shot query |
|---|---|
| Snowflake | SELECT TABLE_NAME, COLUMN_NAME FROM <DB>.INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '<schema>' AND TABLE_NAME IN (...) |
| BigQuery | SELECT table_name, column_name FROM <project>.<dataset>.INFORMATION_SCHEMA.COLUMNS WHERE table_name IN (...) |
| Postgres / Redshift | SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = '<schema>' AND table_name IN (...) |
| Databricks | SELECT table_name, column_name FROM system.information_schema.columns WHERE table_catalog = '<catalog>' AND table_schema = '<schema>' AND table_name IN (...), or DESCRIBE TABLE <catalog>.<schema>.<table> |
If you have no warehouse access, fall back to the rule in Validation & handoff: flag every inferred join key and require confirmation before apply.
Minimal skeleton:
version: "rudder/v1"
kind: "data-graph"
metadata:
name: "<customer>-<domain>-data-graph"
spec:
id: "<customer>-<domain>-data-graph"
account_id: "<warehouse-account-id>"
models:
- id: "<root-entity>"
type: "entity"
primary_id: "<pk-column>"
relationships: [...]
- id: "<related-entity>"
type: "entity"
primary_id: "<pk-column>"
- id: "<event-model>"
type: "event"
timestamp: "<timestamp-column>"For the full annotated template (every field, relationship examples, worked example on Property-vertical data), read references/data-graph-yaml-template.md.
For vertical-specific starting shapes (Property / E-commerce / SaaS), read references/industry-patterns.md — only load the section matching the customer's vertical.
---
Step 4a — Resolve the warehouse account_id
spec.account_id must be the id of the warehouse account the graph runs against. The account only needs to exist — it does not need to be selected in the Data Graph UI first, and you do not need a RETL source to obtain its id.
The authoritative lookup is the CLI:
# --json is required in agent/non-interactive contexts; the plain
# table output needs a TTY and fails when piped.
rudder-cli workspace accounts list --category source --jsonEach line is an account object. Use the `id` field as account_id; use name and options (account, dbname, warehouse, schema) to pick the right one when several accounts exist. --category source lists exactly the RETL-source warehouse accounts that are valid for a Data Graph; drop the filter or use --type snowflake|databricks|... to widen.
Why the CLI and not the MCP: rudder-mcp can only locate accounts reachable through a RETL source or a destination. Accounts created through the Data Graph UI or a standalone warehouse connection are invisible to the MCP — but they do appear in rudder-cli workspace accounts list. When the MCP cannot surface the account, fall back to the CLI (or accept an account_id the user states explicitly).
Building from scratch — no RETL source yet
Common for demo-environment builds (e.g. provisioning a workspace end-to-end before any audience or RETL source exists). There is no RETL source config to read the id from, and the obvious "create a dummy RETL source just to expose the account" step is unnecessary:
1. Ensure the warehouse account exists (created via the dashboard, Terraform, or the Data Graph UI). 2. rudder-cli workspace accounts list --category source --json → take the matching id. 3. Put that id in spec.account_id and proceed to validate / apply.
Note: creating a warehouse destination alone does not always surface a usable account id through every path. The reliable path in all cases is the CLI account list above.
---
Step 5 — Compile the demo warehouse spec
For every model or table referenced in the Data Graph, produce:
- Full table or model name (
schema.tableor model identifier). - Key columns with short descriptions.
- Known types where you can verify them.
- Join keys to other tables.
- Row-count estimate (or a mock-data guidance note).
- Explicit assumptions for anything inferred.
Schema guidance by RETL source type:
- Table sources → use
config.schema+config.table+ warehouse schema lookup when available. - Model sources → parse rendered
config.sqlto recover projected column names / aliases and relationship hints, but do not pretend SQL parsing gives you reliable types. If a type cannot be verified from warehouse metadata, materialization metadata, or explicit docs, mark it asunknownorinferred. - Audience sources → use
config.filterSpec.filterGroups[].filters[].fieldNameonly as evidence of fields already used in segmentation, not as a substitute for full schema.
If you cannot verify a column type, say so. A clearly labeled draft spec is better than invented precision.
---
Step 6 — Identify untapped segments
Do not treat every unused column as an opportunity. The goal is to find realistic segmentation levers, not to diff audience filters against raw schema mechanically.
For each anchor entity, compile candidate segment dimensions from:
- Existing audience
filterSpecfields. - Verified business columns on the underlying entity/model.
- Related entities included in the draft graph.
- Event models and their business timestamps.
Prioritize columns that are typically segmentable:
- lifecycle stage
- plan / tier
- status
- region / market / branch / owner
- product category
- monetary bands or usage bands
- tenure or recency derived from real business timestamps
Usually exclude or de-prioritize:
- raw ids / foreign keys by themselves
- ingestion metadata (
loaded_at,_etl_*,snapshot_date) - high-cardinality free text
- opaque JSON blobs
- sensitive fields unless the customer already uses them intentionally
Output each untapped segment as an idea with a short rationale, for example:
- "Contacts by role" because
roleexists ondim_contactand no current audience references it. - "Branches with no leads in the last 30 days" because
lead_sent_atexists on a related event model and current audiences are single-table only.
Why this matters: this is the strongest upgrade narrative when grounded in the customer's own data model, but it only works if the suggestions are credible.
---
Step 7 — Frame the upgrade conversation
Pair the deliverables above with a capability-gap narrative: what the customer can do today on RETL Audiences vs. what opens up with Data Graph. For the comparison table and talking points for each row, read references/capability-comparison.md.
Ground the framing in:
- the untapped segments from Step 6
- the multi-entity joins they currently cannot express cleanly
- the event and time-window filters already latent in their data
Specifics beat abstract feature lists. Name the first two or three audiences the customer could build immediately after migration.
---
Validation & handoff
Before handing the YAML to the customer:
1. Validate — run the YAML through rudder-cli or import it into the visual builder preview. 2. If you cannot validate, label the output clearly as an unvalidated draft. Do not call it ready-to-use unless a validation step actually happened. 3. Confirm every join key with the customer. Inferred joins are guesses. A wrong join key produces audiences that silently return the wrong population. 4. Flag assumptions explicitly. Any inferred cardinality, guessed account id, unresolved type, or event-timestamp assumption should be called out in the handoff note.
---
Common gotchas
- `includeConfig=false` is not enough for audience extraction, SQL parsing, or
accountIdresolution. Always fetch full config for shortlisted sources. - Warehouse account IDs come from
rudder-cli workspace accounts list --category source --json(theidfield). Don't require a RETL source to exist first, and don't rely on the MCP — it can't see DG-UI or warehouse-connection accounts. See Step 4a. - Templated model SQL must be rendered before column extraction.
- SQL parsing recovers names, not guaranteed types. Mark unverifiable types as unknown / inferred.
- Soft-deleted rows are included unless filtered at the model layer.
- Multi-tenant schemas need the tenant column surfaced so Audiences can scope correctly.
- Device-mode destinations bypass RudderStack servers; they will not explain warehouse-side RETL source shape.
- Date/time dimensions are not graph nodes. Do not model
DIM_DATES/DIM_TIMEor join them at audience time. An event model'stimestampcolumn already encodes the moment, and time-window filters operate on it directly.
---
Reference files
references/data-graph-yaml-template.md— full annotated YAML + worked example + troubleshooting.references/industry-patterns.md— Property, E-commerce, SaaS starting shapes with segment examples.references/capability-comparison.md— RETL Audiences vs. Data Graph table with talking points.
External docs
- Audiences Overview: https://www.rudderstack.com/docs/audiences/overview/
- Data Graph: https://www.rudderstack.com/docs/audiences/data-graph/
- Data Graph YAML reference: https://www.rudderstack.com/docs/audiences/data-graph/cli-reference/
- Visual builder: https://www.rudderstack.com/docs/audiences/data-graph/create-data-graph/
- RETL Audiences (existing): https://www.rudderstack.com/docs/data-pipelines/reverse-etl/features/audiences/
RETL Audiences vs. Data Graph Capability Comparison
Use this to frame the upgrade conversation with customers.
Comparison table
| Capability | RETL Audiences (today) | Data Graph (upgrade) |
|---|---|---|
| Entity sources | Single table/model per audience | Multiple entities with relationships |
| Joins | Manual SQL in model source | Declarative relationships in YAML |
| Events | Not supported | First-class event models with timestamps |
| Time windows | Manual SQL | Built-in recency and window filters |
| Cross-entity filters | Requires custom model | "Customers whose Account is in tier X" |
| Relationship traversal | Not supported | Navigate entity→entity and entity→event |
| Schema visibility | Hidden in SQL | Visual entity map in Audience Builder |
| Maintenance | Edit SQL per audience | Edit graph once, all audiences update |
| Version control | Per-audience | Single YAML spec in git |
| Validation | Runtime errors | CLI validation before deploy |
Talking points by row
Entity sources
Today: Each audience is built on exactly one table or model. To combine customer + account data, you must create a model that joins them upstream.
Upgrade: Define customers and accounts as separate entities with a relationship. The Audience Builder lets marketers filter on both without touching SQL.
Example pitch: "Instead of maintaining a customer_with_account_info model, you define the relationship once and the builder handles the join."
---
Joins
Today: Joins live in SQL inside model sources. Changes require editing SQL and revalidating.
Upgrade: Relationships are declared in YAML with explicit join keys. The system generates correct joins.
Example pitch: "Your SQL models stay simple. Join logic is declarative and auditable in version control."
---
Events
Today: No concept of events. To filter by "ordered in last 30 days," you must pre-compute a flag or aggregate in a model.
Upgrade: Event models have a timestamp field. Time-window filters are native: "Customers who placed an order in the last 30 days."
Example pitch: "You have fact_orders already. With Data Graph, 'active customers' becomes a click, not a SQL change."
---
Time windows
Today: Manual. You write WHERE ordered_at > DATEADD(day, -30, CURRENT_DATE) in a model.
Upgrade: Built-in. Select the event, pick the window, done.
Example pitch: "Marketing can change '30 days' to '7 days' without a deploy cycle."
---
Cross-entity filters
Today: Requires a model that flattens the relationship. "Customers whose Account is Enterprise" means a model with account tier denormalized onto customers.
Upgrade: Traverse relationships in the builder. "Customers → belongs to Account → where tier = 'Enterprise'."
Example pitch: "Your dim_customer stays normalized. Account-level filters work without duplication."
---
Relationship traversal
Today: Not supported. Each audience is a single flat table.
Upgrade: Navigate from entity to entity or entity to event. "Branches → that have Contacts → who received Leads in the last 14 days."
Example pitch: "You can finally answer 'which branches are struggling?' by combining branch, contact, and lead data."
---
Schema visibility
Today: Marketers see a list of raw warehouse columns (EMAIL_ADDRESS, CREATED_TS). Relationships are invisible and column names are cryptic.
Upgrade: Visual entity map shows how entities connect, and per-column aliases (display_name) and descriptions replace cryptic warehouse names with marketer-friendly labels right in the builder — and sensitive columns can be flagged PII (pii_mask) so their values are masked in the Data Graph preview. Marketers understand the data model, and PII stays protected.
Example pitch: "Less 'what column do I use?' tickets. The graph is self-documenting — EMAIL_ADDRESS shows up as 'Email' with a description, no warehouse spelunking."
---
Maintenance
Today: Each audience has its own SQL. A schema change means editing every affected audience.
Upgrade: Edit the Data Graph once. All audiences using that entity update automatically.
Example pitch: "When you rename a column, you fix it in one place."
---
Version control
Today: Audiences are configured in the UI or scattered across model SQL files.
Upgrade: Single YAML spec. PR reviews, git history, rollbacks.
Example pitch: "Your data model is code. Treat it like code."
---
Validation
Today: Errors surface at runtime when the audience query fails.
Upgrade: rudder-cli validate catches errors before deploy: missing columns, invalid joins, type mismatches.
Example pitch: "Catch mistakes in CI, not in production."
---
Framing the conversation
1. Start with their pain. What are they trying to do today that's hard? Multi-entity filters? Time windows? Denormalized models?
2. Map pain to capability. "You mentioned maintaining that joined model is tedious. With Data Graph, you define the relationship once."
3. Show their data. Use the Source Inventory and Untapped Segments from the skill output. "You already have fact_orders. Here's the audience you could build in 5 minutes."
4. Quantify the unlock. "You have 3 audience sources today. With your current entities and events, you could support 12+ segment combinations."
5. Propose next steps. Hand them the draft YAML. Offer to walk through validation. Schedule a builder demo.
Data Graph YAML Template
Full annotated template for Data Graph specs with worked examples and troubleshooting.
Complete annotated template
version: "rudder/v1" # Required. Schema version.
kind: "data-graph" # Required. Resource type.
metadata:
name: "acme-ecommerce-data-graph" # Required. Human-readable name for the graph.
spec:
id: "acme-ecommerce-data-graph" # Required. Unique identifier used in syncs.
account_id: "2abc123xyz" # Required. Warehouse account ID. Resolve via `rudder-cli workspace accounts list --category source --json` (the `id` field).
models:
# ─────────────────────────────────────────────────────────────
# ENTITY: Root entity (the "who" being activated)
# ─────────────────────────────────────────────────────────────
- id: "customers" # Unique model identifier (snake_case or kebab-case).
display_name: "Customers" # UI label. Must be unique across ALL models.
type: "entity" # "entity" = dimension-like table with durable PK.
table: "ACME_DB.ECOMMERCE.DIM_CUSTOMERS" # 3-part: catalog.schema.table
description: "Customer records with demographics and loyalty tier" # Tooltip in Audience Builder.
primary_id: "CUSTOMER_KEY" # Required for entity. Column that uniquely identifies rows.
root: true # Entity only. Marks an audience-builder anchor. Multiple roots allowed; count is not validated.
columns: # Optional. Per-column overrides surfaced in the Audience Builder. Sparse — list only what you relabel or flag.
- name: "EMAIL_ADDRESS" # Warehouse column name (must match the table).
display_name: "Email" # Alias shown instead of the raw column name. ≤255 chars, unique within the model.
description: "Primary contact email" # Note shown alongside the column.
pii_mask: true # Optional. Masks this column's values in the Data Graph preview. Enterprise only.
- name: "LOYALTY_TIER"
display_name: "Loyalty Tier" # Alias only — no description.
- name: "CUSTOMER_NOTES"
description: "Free-form CRM notes" # Description only — no alias.
- name: "SSN"
pii_mask: true # PII only — no alias or description needed.
relationships:
# Entity → Event relationship
- id: "customer-has-orders"
display_name: "Has Orders" # Must be unique across ALL relationships.
cardinality: "one-to-many" # Entity→Event must be one-to-many.
target: "#data-graph-model:orders" # URN syntax required.
source_join_key: "CUSTOMER_KEY" # Column on THIS model.
target_join_key: "CUSTOMER_KEY" # Column on TARGET model.
# Entity → Entity relationship
- id: "customer-belongs-to-account"
display_name: "Belongs To Account"
cardinality: "many-to-one" # Many customers → one account.
target: "#data-graph-model:accounts"
source_join_key: "ACCOUNT_KEY"
target_join_key: "ACCOUNT_KEY"
# ─────────────────────────────────────────────────────────────
# ENTITY: Related entity (FK target)
# ─────────────────────────────────────────────────────────────
- id: "accounts"
display_name: "Accounts"
type: "entity"
table: "ACME_DB.ECOMMERCE.DIM_ACCOUNTS"
description: "B2B account records"
primary_id: "ACCOUNT_KEY"
# No relationships needed if this is a leaf node.
# ─────────────────────────────────────────────────────────────
# EVENT: Timestamped activity table
# ─────────────────────────────────────────────────────────────
- id: "orders"
display_name: "Orders"
type: "event" # "event" = fact-like table with timestamp.
table: "ACME_DB.ECOMMERCE.FACT_ORDERS"
description: "Order transactions"
timestamp: "ORDERED_AT" # Required for event. True business timestamp.
# Events cannot have primary_id.
# Events cannot have relationships to other events.Field reference
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
version | String | Yes | Schema version: "rudder/v1" |
kind | String | Yes | Resource type: "data-graph" |
metadata.name | String | Yes | Human-readable name |
spec.id | String | Yes | Unique identifier for syncs |
spec.account_id | String | Yes | Warehouse account ID — rudder-cli workspace accounts list --category source --json (id field) |
spec.models | List | Yes | Entity and event models |
Model fields
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique model identifier |
display_name | String | Yes | UI label (must be unique across all models) |
type | String | Yes | "entity" or "event" |
table | String | Yes | Fully qualified: catalog.schema.table |
description | String | No | Tooltip in Audience Builder |
primary_id | String | Entity only | Column that uniquely identifies rows |
root | Bool | Entity only, optional | Marks an audience-builder anchor. Multiple roots allowed; the count is not validated (zero, one, or many all pass) |
timestamp | String | Event only | Event timestamp column |
relationships | List | No | Relationships to other models |
columns | List | No | Per-column metadata overrides (aliases, descriptions, PII masking) surfaced in the Audience Builder. See Column metadata fields |
Relationship fields
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique relationship identifier |
display_name | String | Yes | UI label (must be unique) |
cardinality | String | Yes | "one-to-one", "one-to-many", or "many-to-one" |
target | String | Yes | Target model: #data-graph-model:<model-id> |
source_join_key | String | Yes | Column on source model for join |
target_join_key | String | Yes | Column on target model for join |
Column metadata fields
Optional per-column overrides under a model's columns: block. Sparse — list only the columns you want to relabel or flag; unlisted columns keep their raw warehouse names. By default the Audience Builder shows raw warehouse column names; aliases and descriptions make them readable for marketers building audiences and expressions, and pii_mask flags sensitive columns so their values are masked in the Data Graph preview.
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Warehouse column name (must match the model's table) |
display_name | String | Conditional | Alias shown in the Audience Builder instead of the raw column name. ≤255 chars, case-insensitive-unique within the model |
description | String | Conditional | Note shown alongside the column. ≤255 chars; no uniqueness constraint |
pii_mask | Bool | No | When true, masks the column's values (***) in the Data Graph preview. Defaults to false. Enterprise only — the server rejects pii_mask: true on other plans |
Each columns entry must set at least one of display_name, description, or pii_mask (an entry with only name is invalid). apply is declarative — drop a column's entry to clear its metadata. In the preview, masked values show as ***; users with the PII rETL Data Access permission (or enterprise admins) can reveal the clear text.
Validation rules
tablemust be a 3-part reference (catalog.schema.table)- Entity models require
primary_idand must not settimestamp - Event models require
timestampand must not setprimary_id - Event↔event relationships are forbidden
- Event→entity cardinality must be
many-to-one - Entity→event cardinality must be
one-to-many - Entity↔entity can be any cardinality
targetmust use URN syntax#data-graph-model:<model-id>and resolve to a model in the same specdisplay_namemust be unique among models and, separately, among relationships — the two are independent namespaces, so a model and a relationship may share a name, but two models (or two relationships) may not- Declare each relationship once, on a single model — do not also declare its inverse on the target model; the graph traverses it both ways. Double-declaration collides on the relationship
display_nameand inflates the relationship count root: trueis an optional entity-only flag; multiple roots are allowed and the count is not validatedcolumnsis optional and sparse — list only the columns you want to relabel or flag; unlisted columns keep their raw warehouse names- each
columns[]entry needsnameplus at least one ofdisplay_name/description/pii_mask(an entry with onlynameis invalid) columns[].display_nameis case-insensitive-unique within a model — a separate namespace from model and relationshipdisplay_nameuniqueness;descriptionhas no uniqueness constraintcolumns[].pii_maskis an optional boolean (defaultfalse); marking a columnpii_mask: trueis enterprise-only and the server rejects it on other plans
Worked example: Property vertical
A property management company with branches, contacts, and lead events.
version: "rudder/v1"
kind: "data-graph"
metadata:
name: "propco-data-graph"
spec:
id: "propco-data-graph"
account_id: "2wh9abc123"
models:
# Root entity: Branches (the business units)
- id: "branches"
display_name: "Branches"
type: "entity"
table: "PROPCO_DW.ANALYTICS.DIM_BRANCH"
description: "Property management branches"
primary_id: "BRANCH_ID"
root: true
relationships:
- id: "branch-has-contacts"
display_name: "Has Contacts"
cardinality: "one-to-many"
target: "#data-graph-model:contacts"
source_join_key: "BRANCH_ID"
target_join_key: "BRANCH_ID"
- id: "branch-has-leads"
display_name: "Has Leads Sent"
cardinality: "one-to-many"
target: "#data-graph-model:leads-sent"
source_join_key: "BRANCH_ID"
target_join_key: "BRANCH_ID"
# Related entity: Contacts
- id: "contacts"
display_name: "Contacts"
type: "entity"
table: "PROPCO_DW.ANALYTICS.DIM_CONTACT"
description: "Landlords and tenants"
primary_id: "CONTACT_ID"
relationships:
- id: "contact-belongs-to-branch"
display_name: "Belongs To Branch"
cardinality: "many-to-one"
target: "#data-graph-model:branches"
source_join_key: "BRANCH_ID"
target_join_key: "BRANCH_ID"
- id: "contact-has-leads"
display_name: "Has Leads Sent"
cardinality: "one-to-many"
target: "#data-graph-model:leads-sent"
source_join_key: "CONTACT_ID"
target_join_key: "CONTACT_ID"
# Event: Leads sent
- id: "leads-sent"
display_name: "Leads Sent"
type: "event"
table: "PROPCO_DW.ANALYTICS.FACT_LEAD_SENT"
description: "Lead submission events"
timestamp: "SENT_AT"Audiences this graph enables:
1. "Branches with no leads in the last 30 days" — Branch entity + Leads Sent event + time window 2. "Contacts who received a lead but didn't convert" — Contact + Leads Sent + conversion flag 3. "Top-performing branches by lead volume" — Branch + aggregated Leads Sent count
Troubleshooting
"primary_id required for entity type"
You set type: "entity" but forgot primary_id. Add the column that uniquely identifies rows.
"timestamp required for event type"
You set type: "event" but forgot timestamp. Add the business-time column (not loaded_at or snapshot_date).
"Event to event relationships are not allowed"
You tried to create a relationship between two event models. This is forbidden. Events must relate to entities, not to other events.
"Invalid cardinality for entity to event relationship"
Entity→Event must be one-to-many. Event→Entity must be many-to-one. Check the direction.
"Model reference not found"
The target URN doesn't match any id in the same spec. Check for typos. Remember: #data-graph-model:orders must match a model with id: "orders".
"Duplicate display_name"
Two models share a display_name, or two relationships share one. Names must be unique within each type — model names among models, relationship names among relationships. (A model and a relationship may share a name; the namespaces are separate.)
A common trigger is two relationships that naturally share a label — e.g. sale-via-channel and interaction-via-channel both wanting "Via Channel". Prefix with the source entity to disambiguate: "Sale Via Channel" and "Interaction Via Channel". The same fix applies to any pair of symmetric relationships pointing at the same target entity.
"Invalid table reference"
The table must be 3-part: catalog.schema.table. Common mistake: using 2-part schema.table.
Validation passes but audiences return wrong data
Most likely cause: wrong join keys. The YAML is syntactically valid but semantically wrong. Verify join keys match actual FK relationships in the warehouse.
apply fails on a missing / invalid account_id
The account only needs to exist — you do not need to select it in the Data Graph UI, and you do not need a RETL source. List the warehouse accounts and copy the id:
rudder-cli workspace accounts list --category source --jsonrudder-mcp cannot see accounts created through the DG UI or a standalone warehouse connection (it only surfaces accounts behind a RETL source or destination), so the CLI list is the authoritative lookup.
CLI commands
# List warehouse accounts; the `id` field is the spec.account_id value
rudder-cli workspace accounts list --category source --json
# Validate all data-graph specs in current directory
rudder-cli validate -l ./
# Dry run to preview changes
rudder-cli apply --dry-run -l ./
# Apply to workspace
rudder-cli apply -l ./Industry Patterns
Starting shapes for common verticals. Load only the section matching the customer's vertical.
Property Management
Root entity: Branches (the business units being measured and activated)
Typical entities:
branches— office locations, each with staff and a portfoliocontacts— landlords, tenants, vendorsproperties— managed real estate unitsstaff— property managers, agents
Typical events:
leads_sent— lead submission to a branch or agentviewings_booked— property showing scheduledcontracts_signed— lease or management agreement executedmaintenance_requests— tenant service requests
Common graph shape:
branches (entity, root)
├── one-to-many → contacts (entity)
├── one-to-many → properties (entity)
├── one-to-many → leads_sent (event)
└── one-to-many → viewings_booked (event)
contacts (entity)
├── many-to-one → branches (entity)
├── one-to-many → leads_sent (event)
└── one-to-many → contracts_signed (event)
properties (entity)
├── many-to-one → branches (entity)
└── one-to-many → viewings_booked (event)High-value segment ideas:
- Branches with no leads in 30 days
- Landlords who haven't renewed in 12 months
- Properties with high maintenance request volume
- Contacts by branch region + lifecycle stage
---
E-Commerce
Root entity: Customers (the people buying things)
Typical entities:
customers— registered users with profile dataaccounts— B2B accounts (if applicable)products— catalog itemscategories— product groupings
Typical events:
orders— completed purchasescart_additions— product added to cartpage_views— product page visitsreviews_submitted— customer reviews
Common graph shape:
customers (entity, root)
├── many-to-one → accounts (entity) # if B2B
├── one-to-many → orders (event)
├── one-to-many → cart_additions (event)
└── one-to-many → page_views (event)
accounts (entity)
└── one-to-many → customers (entity)
products (entity)
├── many-to-one → categories (entity)
└── one-to-many → orders (event, via line items)High-value segment ideas:
- Customers who added to cart but didn't purchase (last 7 days)
- High-LTV customers by order frequency
- Customers by product category affinity
- Churned customers (no order in 90 days, previously active)
- Customers by region + tier
---
SaaS / Subscription
Root entity: Users or Accounts (depending on B2C vs B2B)
Typical entities:
users— individual product usersaccounts/organizations— paying entities (B2B)workspaces/projects— user-created containersplans— subscription tiers
Typical events:
feature_used— product feature engagementlogins— session startssubscription_changes— upgrades, downgrades, cancellationssupport_tickets— customer service interactions
Common graph shape (B2B):
accounts (entity, root)
├── many-to-one → plans (entity)
├── one-to-many → users (entity)
├── one-to-many → workspaces (entity)
└── one-to-many → subscription_changes (event)
users (entity)
├── many-to-one → accounts (entity)
├── one-to-many → feature_used (event)
└── one-to-many → logins (event)
workspaces (entity)
├── many-to-one → accounts (entity)
└── one-to-many → feature_used (event)High-value segment ideas:
- Accounts approaching usage limits (upsell candidates)
- Users who haven't logged in for 14 days (churn risk)
- Accounts with high feature adoption (case study candidates)
- Trial accounts with high engagement (conversion targets)
- Accounts by plan tier + region
---
Healthcare / Clinics
Root entity: Patients or Providers (depending on use case)
Typical entities:
patients— individuals receiving careproviders— doctors, nurses, stafflocations— clinics, hospitalsinsurance_plans— payer information
Typical events:
appointments— scheduled visitsvisits_completed— actual encountersprescriptions— medication orderslab_results— test completions
Common graph shape:
patients (entity, root)
├── many-to-one → locations (entity, primary clinic)
├── one-to-many → appointments (event)
├── one-to-many → visits_completed (event)
└── one-to-many → prescriptions (event)
providers (entity)
├── many-to-one → locations (entity)
└── one-to-many → visits_completed (event)
locations (entity)
├── one-to-many → patients (entity)
└── one-to-many → appointments (event)High-value segment ideas:
- Patients overdue for annual checkup
- Patients by chronic condition + last visit date
- High-utilization patients (care management candidates)
- Patients by provider + location
---
Selecting the right root entity
The root entity should be:
1. The "who" you activate on — who receives the marketing message, the personalization, the outreach? 2. Stable and well-keyed — has a reliable primary key that doesn't change. 3. Rich with relationships — connects to events and other entities that enable interesting segments.
If unsure, ask: "When this customer builds an audience, what are they counting? Branches? Customers? Users?" That's the root.