rudder-data-graphs — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited rudder-data-graphs (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Given a customer name (or workspace / org identifier), this skill produces five deliverables, in order:
rudder-cli or the visual builder.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.
admin_search_organizations(company_name=...) to get the org_id.admin_search_workspaces(search_by=organizationId) using that id.status=ACTIVE workspaces. Why: inactive workspaces often have stale or disconnected configs and will produce misleading source lists.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.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:
config.filterSpec.filterGroups[].filters[].fieldName — the fields the customer actually filters on.primaryKey and any source-level accountId if present.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.contact has a branch_id, pull in branch). Pull these in.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 |
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:
dim_contact is synced to HubSpot, contacts are an activation entity. Treat those tables/models as anchor entities.Every shortlisted source becomes one of:
Edge cases (these come up often enough to call out):
*_KEY), a stable business id, and effective/end-date columns, use the *surrogate `_KEY as primary_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.event_at, ordered_at), use it for the event's timestamp, never the ingestion column.is_deleted / deleted_at column, the Data Graph will include them unless filtered. Note this; the customer may want a filter at the model layer.tenant_id), surface that column in the entity shape and call out scoping expectations.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: true on the entity model (entity-only field). Multiple roots are valid — each root: true entity 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 set root deliberately on whichever entities the builder should start from rather than relying on a default.Relationship direction — common footgun:
source_join_key is the column on the current model (the one declaring the relationship).target_join_key is the column on the target model.branch → contacts is one-to-many from the branch's perspective.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.
account_idspec.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).
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:
rudder-cli workspace accounts list --category source --json → take the matching id.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.
For every model or table referenced in the Data Graph, produce:
schema.table or model identifier).Schema guidance by RETL source type:
config.schema + config.table + warehouse schema lookup when available.config.sql to 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 as unknown or inferred.config.filterSpec.filterGroups[].filters[].fieldName only 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.
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:
filterSpec fields.Prioritize columns that are typically segmentable:
Usually exclude or de-prioritize:
loaded_at, _etl_*, snapshot_date)Output each untapped segment as an idea with a short rationale, for example:
role exists on dim_contact and no current audience references it.lead_sent_at exists 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.
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:
Specifics beat abstract feature lists. Name the first two or three audiences the customer could build immediately after migration.
Before handing the YAML to the customer:
rudder-cli or import it into the visual builder preview.accountId resolution. Always fetch full config for shortlisted sources.rudder-cli workspace accounts list --category source --json (the id field). 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.DIM_DATES / DIM_TIME or join them at audience time. An event model's timestamp column already encodes the moment, and time-window filters operate on it directly.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.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.