Per-customer scoping

How the customer's external ID and JWT attributes flow into your dataset SQL.

Every embed token carries two scope values your dataset SQL can filter on:

  • {{trend.external_id}} — the customer's identifier from your system (the externalId you passed when creating the customer). Always present. Use this when your data is keyed on that same identifier — no configuration needed.
  • {{trend.attributes.<key>}} — a free-form JSON object your backend sets at mint time. Use this for anything beyond identity: region, tier, feature flags, or a different identifier your data is keyed on.

The simple case — scope by external ID

If your orders table has a customer_id column that matches the external ID you gave trend when you created the customer, filter with {{trend.external_id}} directly:

SELECT * FROM orders
WHERE customer_id = {{trend.external_id}}

No attributes to configure. Every customer you create automatically has an external_id, so this always works.

Defining attributes

Use attributes when you need to scope on something OTHER than the external ID — a different foreign-key column, a region, a plan tier, etc. Attributes are a flat JSON object your backend sends when calling POST /api/embed/token:

{
  "externalId": "acme-corp",
  "attributes": {
    "region": "us-east",
    "plan": "enterprise"
  }
}

Keys must look like SQL identifiers (letters, digits, underscores; no leading digit). Values can be strings, numbers, booleans, arrays, or nested objects.

Referencing attributes in dataset SQL

Use the trend.attributes. prefix. trend parameter-binds these — they never get concatenated into your query text:

SELECT date_trunc('day', created_at) AS day,
       sum(amount) AS revenue
FROM orders
WHERE company_id = {{trend.attributes.company_id}}
  AND region = {{trend.attributes.region}}
  AND created_at >= now() - interval '30 days'
GROUP BY 1
ORDER BY 1

If a customer’s token supplies company_id: "acme-corp", the query runs as WHERE company_id = $1 with acme-corp as the bound parameter.

Optional clauses

Wrap conditional filters in [[...]] — the block gets dropped when its variables are missing. Useful for chart-level filters that come from dashboard controls:

SELECT * FROM events
WHERE company_id = {{trend.attributes.company_id}}
  [[ AND event_type = {{event_type}} ]]

Warning

Don’t wrap {{trend.attributes.*}} references in [[...]]. If the token doesn’t supply the attribute, trend hard-rejects the query with a 400 — but if you wrap it, the block silently drops and the WHERE disappears, returning every customer’s rows. The extractor catches wrapped references at the API boundary and still rejects, but writing the SQL correctly (attribute filters outside [[...]], control filters inside) makes the intent obvious.

Scoping is your responsibility

trend runs your dataset SQL verbatim — we don't rewrite it, and we don't force a WHERE clause on your behalf. That means the filter that isolates one customer's rows from another's has to come from your SQL. If your dataset is SELECT * FROM orders with no scoping clause, every embed customer viewing a chart or asking an AI question against it will see rows from all customers.

We can't detect intent — sometimes an unscoped dataset is exactly what you want (a public benchmark visible to every customer, a marketplace-wide leaderboard). So the safety model is: you own the scoping decision, and the UI makes the state visible.

The embed-safety badge

When you edit a dataset's semantic layer and set ai_enabled: true, trend classifies your SQL and shows one of three badges next to the "AI enabled" status:

  • 🟢 Embed-safe — SQL references {{trend.external_id}}. Every embed customer sees only their own rows. This is the recommended shape.
  • 🟡 Attribute-scoped — SQL references {{trend.attributes.*}} but not {{trend.external_id}}. The attribute(s) may or may not isolate customers — {{trend.attributes.tenant_id}} does; {{trend.attributes.region}} doesn't. Verify your attributes uniquely identify each customer.
  • 🔴 Unscoped for embed — SQL has neither. Embed customers will see rows from every customer.

Hover the badge for a full explanation of what each state means for embed traffic.

The unscoped-save confirmation

If you try to save ai_enabled: true on an unscoped dataset, trend pops a confirmation dialog explaining the risk and requires an explicit "Save anyway" click. Cancel returns you to the editor with the change unsaved so you can add a scoping filter.

Nothing here is a hard block — if you legitimately want an unscoped dataset available to embed AI (again: benchmarks, leaderboards), just confirm. But the dialog exists so an unscoped-by-accident save requires deliberate acknowledgment.

What trend does enforce

The badge and dialog are guides. What trend does enforce automatically at the query API:

  1. Extract every {{trend.attributes.<key>}} reference from the dataset SQL (including references inside [[...]] blocks).
  2. Diff against the keys the token supplied.
  3. If any reference is missing an attribute in the token, return 400 Missing required attributes: <list>.

So if your dataset says WHERE tenant_id = {{trend.attributes.tenant_id}} and a token arrives without a tenant_id attribute, the query is rejected — not silently run with the filter dropped.

Then the same three-layer SQL safety as any internal chart query applies: SELECT-only validator, session-level default_transaction_read_only, and the DB-level read-only role you set up for the connection.

Use {{trend.external_id}} whenever your data model has a column that matches the identifier you passed to create_embed_customer. It works without any attribute configuration and gets you the 🟢 Embed-safe badge automatically. Fall back to attributes only when your data uses a different value or when you need multi-value scoping (e.g., WHERE account_id IN {{trend.attributes.account_ids}}).

Observed customers

Every mint auto-populates a row in Embed → Customers with the external ID and a "last seen" timestamp. No manual registration required — the page is an audit view showing which external IDs you’ve issued tokens for.

trend