diff --git a/.seo/briefs/composite-primary-key-ai-agents.md b/.seo/briefs/composite-primary-key-ai-agents.md
new file mode 100644
index 00000000..b2a8e017
--- /dev/null
+++ b/.seo/briefs/composite-primary-key-ai-agents.md
@@ -0,0 +1,55 @@
+# Composite Primary Keys for AI-Agent Datasets
+
+## Selection
+
+- Target: `composite primary key`
+- Type: definition / implementation decision guide
+- Live US metrics checked 2026-08-06: volume 590, KD 0, informational intent
+- SERP shape: AI Overview/PAA plus definition and implementation tutorials; primary documentation
+ from PostgreSQL, Django, and Rails is prominent.
+- Product-led fit: Rowset exposes one stable index column for MCP/REST by-index operations, so users
+ with multi-field source identity need a deterministic adapter rather than generic SQL advice.
+
+## Information gain
+
+The top results explain native database composite keys. This piece adds the SCOPE identity contract:
+Scope, Components, Ownership, Percent-encode, and Evolution. It shows how to preserve a durable
+identity tuple as one reversible Rowset index without claiming native multi-column key support.
+
+## Table stakes and gap
+
+Table stakes: definition, uniqueness/non-null behavior, examples, when composite identity fits,
+tradeoffs with surrogate IDs, implementation example, and FAQ.
+
+Gap: deterministic construction at an agent tool boundary, delimiter-collision prevention,
+component authority, encoding-version migration, retry behavior, and Rowset relationship semantics.
+
+## Entity map
+
+Composite primary key, primary key, business key, surrogate key, unique constraint, non-null,
+multi-column key, index column, deterministic encoding, percent-encoding, UTF-8, component order,
+normalization, tenant/workspace scope, idempotency, relationship, foreign key, MCP, REST, `rowset_id`.
+
+Questions: What is a composite primary key? Can a primary key contain multiple columns? When should
+you use a composite key? Is it better than a surrogate key? How should an agent encode a composite
+identity when an API accepts one index value?
+
+## Verified claim ledger
+
+| Claim | Source | Tier / date | Verification |
+|---|---|---|---|
+| PostgreSQL primary keys may span multiple columns; the group must be unique and non-null. | https://www.postgresql.org/docs/current/ddl-constraints.html | primary, checked 2026-08-06 | verified primary |
+| Django supports `CompositePrimaryKey`, while current docs list migration and relationship limitations. | https://docs.djangoproject.com/en/6.0/topics/composite-primary-key/ | primary, checked 2026-08-06 | verified primary; release introduction cross-checked at https://www.djangoproject.com/weblog/2025/apr/02/django-52-released/ |
+| Percent-encoding represents data characters that would otherwise conflict with URI syntax. | https://www.rfc-editor.org/rfc/rfc3986 | primary, January 2005 | verified primary |
+| RFC 8785 defines invariant JSON serialization for repeatable cryptographic operations. | https://www.rfc-editor.org/rfc/rfc8785 | primary, June 2020 | verified primary |
+| Rowset accepts one explicit `index_column`, generates `rowset_id` when omitted, and supports by-index operations. | `apps/pages/content/docs/dataset-api.md`; `TECH.md` | product source, checked 2026-08-06 | verified in docs and implementation |
+| Rowset relationships store target dataset index values. | `apps/pages/content/docs/link-datasets.md`; `apps/pages/content/docs/dataset-api.md` | product source, checked 2026-08-06 | verified in two repo docs |
+| The SCOPE contract and `ck1` encoding are this article's proposed framework, not a Rowset protocol guarantee. | article methodology | original analysis, 2026-08-06 | clearly framed as recommendation |
+
+## Side checks
+
+- AI SEO: direct definition first; self-contained answer blocks; current primary sources; explicit
+ entities; visible date through frontmatter; FAQ; renderer-supported `BlogPosting` schema.
+- Product-led SEO: solves exact Rowset lookup, retry, and relationship work; links to the Dataset
+ API, schema design, identity migration, idempotency, relationships, quickstart, and pricing;
+ states that Rowset does not expose native multi-column SQL keys.
diff --git a/.seo/config.json b/.seo/config.json
index 840a8e03..bc19ef58 100644
--- a/.seo/config.json
+++ b/.seo/config.json
@@ -37,10 +37,10 @@
"content_ledger_dataset_key": "9ae7d0d3-1138-43a0-b3b8-aa05fac12a96",
"research_briefs_dataset_key": "3a004cfa-0a7e-4c3a-a517-caca902f38d6",
"schema_version": 1,
- "last_sync_at": "2026-08-04"
+ "last_sync_at": "2026-08-06"
},
"created_at": "2026-07-04",
- "updated_at": "2026-08-04",
+ "updated_at": "2026-08-06",
"notes": [
"Initialized from latest origin/main after fast-forwarding from c11f1de to 2d9389b.",
"Refreshed from current origin/main b93e5d6 with GSC, Plausible, PostHog, DataForSEO, Exa, Firecrawl, Jina, and live HTTP checks on 2026-07-15.",
diff --git a/.seo/content-ledger.md b/.seo/content-ledger.md
index a2ad862f..36493332 100644
--- a/.seo/content-ledger.md
+++ b/.seo/content-ledger.md
@@ -39,6 +39,7 @@
| 2026-08-02 | MCP OAuth vs API Keys: Choose the Right Auth | comparison / decision guide | `/blog/mcp-oauth-vs-api-keys` | MCP OAuth | 390 | 9 | MCP setup, agent access, MCP vs REST, safe sharing, pricing | #392 |
| 2026-08-03 | AI Agent Structured Output: Validate Before You Store | how-to / operational decision guide | `/blog/ai-agent-structured-output` | AI agent structured output | unmeasured | n/a | schema design, data entry, idempotent updates, MCP, Dataset API, pricing | #396 |
| 2026-08-04 | Migrate an AI-Agent Dataset to a Business Key | how-to / operational decision guide | `/blog/migrate-agent-dataset-business-key` | business key database | 10 | n/a | index-column guide, rowset_id vs business keys, schema design, row operations, idempotent updates, MCP, pricing | #397 |
+| 2026-08-06 | Composite Primary Keys for AI-Agent Datasets | definition / implementation decision guide | `/blog/composite-primary-key-ai-agents` | composite primary key | 590 | 0 | index-column guide, business-key migration, Dataset API, schema design, idempotency, relationships, pricing, quickstart | #399 |
---
@@ -73,6 +74,7 @@
| 25 | MCP security best practices | security guide | MCP security best practices | 110 | 27 | commercial | 14 | Defer: KD is above the current authority band and the broad topic would overlap existing safe-sharing, approval, audit, and authentication content. |
| 26 | AI agent structured output | how-to / operational decision guide | AI agent structured output | unmeasured | n/a | implementation / informational | shipped (19) | Selected 2026-08-03 after live SERP research. Product-led angle: a shape -> meaning -> write contract that connects provider schema output to staged, retry-safe Rowset rows without treating valid JSON as proof of correctness. |
| 27 | Migrate an AI-agent dataset to a business key | how-to / operational decision guide | business key database | 10 | n/a | commercial / implementation | shipped (18) | Selected 2026-08-04 from the coverage map's highest-priority open product-native gap. Differentiated with a map -> mirror -> verify -> cut over -> retire identity migration contract. |
+| 28 | Composite primary keys for AI-agent datasets | definition / implementation decision guide | composite primary key | 590 | 0 | informational / implementation | shipped (20) | Selected from the highest-priority open product-native coverage gap. Differentiated with a Scope -> Components -> Ownership -> Percent-encode -> Evolution identity contract for single-index agent tools. |
---
@@ -80,7 +82,7 @@
| Cluster / theme | Pieces shipped | Gaps still open |
|---|---|---|
-| Agent-managed datasets | `/blog/agent-managed-datasets`, `/blog/choose-index-column-agent-rows`, `/blog/structure-dataset-instructions-ai-agents`, `/blog/rowset-id-vs-business-keys`, `/blog/relationship-modeling-agent-datasets`, `/blog/ai-agent-memory-vs-state`, `/blog/idempotent-ai-agent-updates`, `/blog/share-ai-agent-data-safely`, `/blog/human-in-the-loop-ai-agents`, `/blog/ai-agent-audit-trail`, `/blog/database-for-ai-agents`, `/blog/ai-data-cleaning-agent`, `/blog/ai-agent-crm`, `/blog/ai-agent-task-management`, `/blog/ai-agent-inventory-management`, `/blog/ai-customer-feedback-analysis`, `/blog/ai-ready-data`, `/blog/ai-agent-data-entry`, `/blog/ai-data-collection`, `/blog/migrate-agent-dataset-business-key` | Composite-key and cross-system identity patterns |
+| Agent-managed datasets | `/blog/agent-managed-datasets`, `/blog/choose-index-column-agent-rows`, `/blog/structure-dataset-instructions-ai-agents`, `/blog/rowset-id-vs-business-keys`, `/blog/relationship-modeling-agent-datasets`, `/blog/ai-agent-memory-vs-state`, `/blog/idempotent-ai-agent-updates`, `/blog/share-ai-agent-data-safely`, `/blog/human-in-the-loop-ai-agents`, `/blog/ai-agent-audit-trail`, `/blog/database-for-ai-agents`, `/blog/ai-data-cleaning-agent`, `/blog/ai-agent-crm`, `/blog/ai-agent-task-management`, `/blog/ai-agent-inventory-management`, `/blog/ai-customer-feedback-analysis`, `/blog/ai-ready-data`, `/blog/ai-agent-data-entry`, `/blog/ai-data-collection`, `/blog/migrate-agent-dataset-business-key`, `/blog/composite-primary-key-ai-agents` | Cross-system identity patterns beyond composite keys |
| MCP and Dataset API | `/blog/mcp-vs-rest-ai-agents`, `/blog/connect-ai-agent-to-dataset-api`, `/blog/relationship-modeling-agent-datasets`, `/blog/ai-agent-inventory-management`, `/blog/ai-agent-data-entry`, `/blog/mcp-oauth-vs-api-keys`, `/blog/ai-agent-structured-output` | More REST/MCP setup examples with concrete datasets |
| Spreadsheet/database alternatives | `/blog/airtable-alternatives`, `/blog/google-sheets-alternatives`, `/blog/baserow-alternatives`, `/blog/nocodb-alternatives`, `/blog/spreadsheet-database-for-ai-agents`, `/vs/airtable`, `/vs/google-sheets` | Migration examples for specific human-to-agent workflows |
diff --git a/.seo/link-inventory.md b/.seo/link-inventory.md
index 14628d22..a5d769d3 100644
--- a/.seo/link-inventory.md
+++ b/.seo/link-inventory.md
@@ -94,6 +94,7 @@
| `/blog/mcp-oauth-vs-api-keys` | https://rowset.lvtd.dev/blog/mcp-oauth-vs-api-keys | MCP OAuth vs API Keys: Choose the Right Auth | MCP OAuth vs API keys, MCP authorization decision guide, provisioned API key for agents, delegated MCP access, agent authentication delegation test |
| `/blog/ai-agent-structured-output` | https://rowset.lvtd.dev/blog/ai-agent-structured-output | AI Agent Structured Output: Validate Before You Store | AI agent structured output, structured output validation for agents, shape meaning write contract, JSON Schema to durable rows, validated agent output workflow |
| `/blog/migrate-agent-dataset-business-key` | https://rowset.lvtd.dev/blog/migrate-agent-dataset-business-key | Migrate an AI-Agent Dataset to a Business Key | business-key migration for agent datasets, migrate generated row identity, agent dataset index migration, map mirror verify cutover retire, move from rowset_id to a business key |
+| `/blog/composite-primary-key-ai-agents` | https://rowset.lvtd.dev/blog/composite-primary-key-ai-agents | Composite Primary Keys for AI-Agent Datasets | composite identity, deterministic row index, multi-field business key, SCOPE identity contract |
## Shipped SEO Sprint Pages
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aea70f5f..61894203 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,12 @@ deployment or release cut date.
**Fixed** for any bug fixes.
**Security** in case of vulnerabilities.
+## 2026-08-06
+
+### Added
+- Added a composite-primary-key guide for agent-managed datasets with a deterministic SCOPE
+ identity contract, collision-safe component encoding, retry guidance, and Rowset examples.
+
## 2026-08-04
### Added
diff --git a/apps/pages/content/blog/choose-index-column-agent-rows.md b/apps/pages/content/blog/choose-index-column-agent-rows.md
index 85d91e21..89361863 100644
--- a/apps/pages/content/blog/choose-index-column-agent-rows.md
+++ b/apps/pages/content/blog/choose-index-column-agent-rows.md
@@ -99,6 +99,11 @@ Use this checklist before creating an agent-managed dataset.
If a candidate fails any of those tests, it can still be a useful column. It
just should not be the index.
+Sometimes no single field is unique, but a stable tuple is. The guide to
+[composite primary keys for AI-agent datasets](/blog/composite-primary-key-ai-agents) shows how to
+keep those component fields and derive one deterministic index for a single-index MCP or REST
+surface.
+
## Good index columns by workflow
The right index depends on the job the dataset is doing.
diff --git a/apps/pages/content/blog/composite-primary-key-ai-agents.md b/apps/pages/content/blog/composite-primary-key-ai-agents.md
new file mode 100644
index 00000000..18e79a28
--- /dev/null
+++ b/apps/pages/content/blog/composite-primary-key-ai-agents.md
@@ -0,0 +1,316 @@
+---
+title: "Composite Primary Keys for AI-Agent Datasets"
+description: "Model multi-field row identity for AI agents with a deterministic composite index, explicit component rules, and safe Rowset lookups."
+published_at: 2026-08-06
+updated_at: 2026-08-06
+author: Rasul Kireev
+keywords:
+ - composite primary key
+ - AI agent dataset identity
+ - composite business key
+ - deterministic row index
+topics:
+ - agent workflows
+ - dataset operations
+ - row identity
+canonical_url: https://rowset.lvtd.dev/blog/composite-primary-key-ai-agents
+image: /static/vendors/images/logo.png
+image_alt: Rowset logo
+robots: index, follow
+---
+
+A composite primary key identifies one row with a fixed combination of two or more fields. For an
+AI-agent dataset, use composite identity only when no component is unique by itself and the whole
+tuple is stable. If the storage API accepts one index column, encode that tuple deterministically
+and keep the original components as separate fields.
+
+An inventory record may be unique only within a workspace, so `(workspace_id, sku)` is the real
+identity. A translation may be unique only within a document and locale, so
+`(document_id, locale)` identifies it. Flattening either tuple into an improvised string can make
+agent retries unsafe unless every writer follows the same construction rule.
+
+This guide uses a five-part **SCOPE identity contract**:
+
+1. **Scope:** name the boundary inside which the record is unique.
+2. **Components:** fix the ordered fields that form identity.
+3. **Ownership:** record which system owns each component.
+4. **Percent-encode:** turn the tuple into one reversible, delimiter-safe index.
+5. **Evolution:** version the encoding and migrate instead of silently changing it.
+
+## In this guide
+
+- [What is a composite primary key?](#what-is-composite-primary-key)
+- [When does an AI-agent dataset need one?](#when-agents-need-composite-identity)
+- [The SCOPE identity contract](#scope-identity-contract)
+- [Build a deterministic composite index](#build-composite-index)
+- [Create the Rowset dataset](#create-rowset-dataset)
+- [Handle retries and relationships](#retries-and-relationships)
+- [When to use a generated ID instead](#when-to-use-generated-id)
+- [Composite primary key FAQ](#composite-primary-key-faq)
+
+
+## What is a composite primary key?
+
+A composite primary key is a primary key made from multiple columns whose combined values uniquely
+identify a row. Each component alone may repeat; the complete tuple must be unique and non-null.
+
+PostgreSQL documents primary keys as a column or group of columns whose values uniquely identify
+rows, and it supports declarations such as `PRIMARY KEY (workspace_id, sku)`
+([PostgreSQL 18 constraints documentation, checked August
+2026](https://www.postgresql.org/docs/current/ddl-constraints.html)). In a relational database,
+the database enforces the tuple directly.
+
+Framework support is a separate concern. Django added `CompositePrimaryKey` in version 5.2, but
+its current documentation still lists migration and relationship limitations
+([Django composite primary key documentation, checked August
+2026](https://docs.djangoproject.com/en/6.0/topics/composite-primary-key/)). That distinction
+matters for agents: a valid database design can still be awkward at the API or tool boundary.
+
+Rowset exposes one `index_column` for exact row lookup. It does not claim to provide a native
+multi-column SQL primary key. When your source identity is a tuple, the practical Rowset pattern is
+to retain the component columns and derive one deterministic text index from them.
+
+
+## When does an AI-agent dataset need composite identity?
+
+Use composite identity when the workflow naturally names a record with more than one stable value.
+Common examples include:
+
+| Workflow | Identity tuple | Why one field is insufficient |
+|---|---|---|
+| Multi-workspace catalog | `(workspace_id, sku)` | The same SKU can exist in different workspaces. |
+| Localized content | `(content_id, locale)` | One content item has several locale-specific rows. |
+| Daily measurement | `(device_id, observed_date)` | A device has one accepted measurement per day. |
+| Membership record | `(team_id, person_id)` | A person can belong to several teams. |
+| Source synchronization | `(source_system, source_id)` | Different systems can issue the same local ID. |
+
+Do not add components merely because they are available. If `event_id` is already globally unique,
+then `(workspace_id, event_id)` usually gives agents a wider, more fragile handle without improving
+identity. The [index-column decision guide](/blog/choose-index-column-agent-rows) applies the same
+required, unique, stable, and recognizable tests to single-field keys.
+
+Also separate identity from state. `(task_id, status)` is a poor composite key because `status` is
+supposed to change. `(customer_id, email)` is risky when email can be corrected. Every component
+becomes part of the lookup contract, so changing one changes the row's apparent identity.
+
+
+## Use the SCOPE identity contract
+
+The SCOPE contract turns a plausible tuple into a specification an agent can follow after its
+current conversation ends.
+
+### 1. Scope: state the uniqueness boundary
+
+Write the sentence that explains where the record is unique:
+
+```text
+A catalog item is unique inside one workspace by the pair
+(workspace_id, sku).
+```
+
+If you cannot name the boundary, you probably have not found the identity yet. `sku` may be unique
+per supplier rather than per workspace. A locale code may need a document version. Resolve that
+ambiguity before creating rows.
+
+### 2. Components: fix names, order, and validation
+
+List the components in one order and never let callers rearrange them. For example:
+
+```text
+components = [workspace_id, sku]
+workspace_id = required, exact lowercase slug
+sku = required, case-sensitive supplier value
+```
+
+`(workspace_id, sku)` and `(sku, workspace_id)` contain the same values but must not produce two
+indexes. The contract also decides whether whitespace, case, Unicode, and leading zeroes are
+meaningful. Do not let each agent normalize those details independently.
+
+### 3. Ownership: identify who may issue or change each value
+
+Name the authority for every component. Rowset may store `workspace_id`, but the workspace service
+owns it. An agent may transport a supplier SKU, but the supplier catalog defines its exact value.
+
+Ownership prevents a well-meaning agent from inventing `unknown-workspace`, trimming a significant
+leading zero, or lowercasing an identifier simply to make validation pass. Missing identity should
+become a typed exception or review item, not fabricated data.
+
+### 4. Percent-encode: make one reversible index
+
+When an API accepts one text index, encode each component separately and join the encoded values
+with a reserved delimiter. RFC 3986 defines percent-encoding for representing data characters that
+would otherwise conflict with URI syntax
+([RFC 3986, January 2005](https://www.rfc-editor.org/rfc/rfc3986)).
+
+For this contract, encode the UTF-8 form of every component and join them with `|`. Because a raw
+pipe inside a component becomes `%7C`, splitting the finished index on `|` remains unambiguous.
+
+### 5. Evolution: version the format
+
+Prefix the result with an encoding version such as `ck1`. The prefix is not a schema version for
+the entire dataset. It tells a future reader exactly how to decode this identity string.
+
+If component order or normalization changes, create `ck2` values in a new dataset and use an
+explicit migration. Do not reinterpret existing `ck1` values in place. The
+[business-key migration guide](/blog/migrate-agent-dataset-business-key) covers mapping, mirrored
+writes, verification, cutover, and rollback.
+
+
+## How do you build a deterministic composite index?
+
+Build the index in one shared function, reject blank components, and test collision-shaped inputs.
+This Python example uses only the standard library:
+
+```python
+from urllib.parse import quote
+
+
+def catalog_index(workspace_id: str, sku: str) -> str:
+ components = (workspace_id, sku)
+ if any(value == "" for value in components):
+ raise ValueError("Composite identity components must be non-blank")
+ encoded = (quote(value, safe="") for value in components)
+ return "ck1|" + "|".join(encoded)
+```
+
+The same values always produce the same lookup handle:
+
+| `workspace_id` | `sku` | `composite_id` |
+|---|---|---|
+| `acme` | `SKU-104` | `ck1|acme|SKU-104` |
+| `acme` | `US|42` | `ck1|acme|US%7C42` |
+| `acme east` | `A/B` | `ck1|acme%20east|A%2FB` |
+
+Avoid raw concatenation such as `workspace_id + "-" + sku`. The tuples `("a-b", "c")` and
+`("a", "b-c")` both flatten to `a-b-c`. JSON stringification is safer than raw concatenation,
+but different serializers can vary in whitespace and escaping unless you define a canonical form.
+RFC 8785 exists because repeatable hashing and signing require invariant JSON serialization
+([JSON Canonicalization Scheme, June 2020](https://www.rfc-editor.org/rfc/rfc8785)). For a short
+tuple of text identifiers, per-component encoding is easier to inspect and reproduce.
+
+Test the encoder with delimiter characters, percent signs, spaces, non-ASCII text, case variants,
+and leading zeroes. Test that approved normalization is applied before encoding and that every
+unapproved transformation is rejected.
+
+
+## How do you store composite identity in Rowset?
+
+Keep both the derived index and its source components. The derived field powers exact lookup; the
+component fields keep exports, reviews, and relationships understandable.
+
+```json
+{
+ "name": "Workspace catalog",
+ "description": "Catalog records unique by workspace and supplier SKU",
+ "instructions": "Use composite_id for exact lookup. Build it as ck1|percent_encode(workspace_id)|percent_encode(sku). Preserve component case. Never invent missing identity values.",
+ "headers": ["composite_id", "workspace_id", "sku", "name", "status"],
+ "index_column": "composite_id",
+ "column_types": {
+ "composite_id": {
+ "type": "text",
+ "description": "Versioned deterministic index derived from workspace_id and sku"
+ },
+ "workspace_id": {
+ "type": "text",
+ "description": "Exact workspace slug issued by the workspace service"
+ },
+ "sku": {
+ "type": "text",
+ "description": "Case-sensitive supplier SKU"
+ },
+ "status": {
+ "type": "choice",
+ "choices": ["active", "inactive"]
+ }
+ },
+ "metadata": {
+ "identity": {
+ "version": "ck1",
+ "components": ["workspace_id", "sku"],
+ "separator": "|",
+ "encoding": "RFC 3986 percent-encoding over UTF-8"
+ }
+ }
+}
+```
+
+Rowset's [Dataset API](/docs/dataset-api) accepts one explicit `index_column` at creation. The
+[schema design guide](/docs/design-schema) explains how column descriptions, dataset instructions,
+and JSON metadata give future agents durable field meaning and workflow rules.
+
+Generate `composite_id` before the write. After creating or updating a row, read it back by that
+index and compare `workspace_id` and `sku` with the payload. Treat a mismatch as a contract failure,
+not as permission to overwrite the components.
+
+
+## How does composite identity affect retries and relationships?
+
+A deterministic composite index gives every retry the same target. If a create response is lost,
+the agent can rebuild `ck1|acme|SKU-104`, look up the row, compare the stored values, and then decide
+whether a new write is necessary. The [idempotent update guide](/blog/idempotent-ai-agent-updates)
+shows the full inspect, compare, mutate, and verify sequence.
+
+Relationships need the same discipline. Rowset relationships store a target dataset's index value,
+so a source row should keep the complete `composite_id`, not one component or a display name. The
+[relationship documentation](/docs/link-datasets) covers enforcement and resolution through MCP
+and REST.
+
+If another system already supports multi-column foreign keys, keep using its native tuple there.
+The encoded Rowset index is an adapter for a single-index tool boundary, not a reason to flatten the
+source database's relational design.
+
+
+## When should you use a generated ID instead?
+
+Use a generated `rowset_id` or a stable upstream surrogate ID when the proposed tuple is mutable,
+partially unknown, excessively long, or inconsistently normalized across systems. Keep a separate
+uniqueness rule over the business fields in the source system when it can enforce one.
+
+Choose generated identity when:
+
+- one or more components can change during normal work
+- the identity authority cannot define canonical case or whitespace rules
+- agents often receive only part of the tuple
+- downstream tools cannot reproduce the encoding exactly
+- the source already provides a globally unique immutable ID
+
+The [Rowset ID versus business-key guide](/blog/rowset-id-vs-business-keys) explains that tradeoff.
+Rowset can generate `rowset_id` when `index_column` is omitted, giving the agent a stable internal
+handle without pretending uncertain business fields form safe identity.
+
+Use native composite primary keys in a relational database when the database should enforce the
+tuple directly. Use a deterministic composite index in Rowset when trusted agents need one exact
+lookup value across MCP or REST. Those are compatible choices at different boundaries.
+
+Review [Rowset pricing](/pricing) if you want to operate the pattern with private hosted datasets,
+or start with the [Rowset quickstart](/docs/quickstart) to create and inspect a dataset through an
+agent.
+
+
+## Composite primary key FAQ
+
+### Can a primary key contain multiple columns?
+
+Yes. PostgreSQL and other relational databases can enforce one primary key across a group of
+columns. The combined tuple must uniquely identify each row, and primary-key components cannot be
+null. Framework and API support may impose additional limitations even when the database supports
+the design.
+
+### Is a composite primary key better than a surrogate key?
+
+Neither is universally better. Use composite identity when a stable real-world tuple is the record's
+actual identity and every caller can provide it. Use a surrogate key when the tuple is mutable,
+wide, optional, or difficult to carry through APIs and relationships. You may still enforce a
+separate unique constraint over business fields in the source database.
+
+### Does Rowset support a native multi-column primary key?
+
+Rowset exposes one index column for exact by-index operations. To preserve multi-field identity,
+keep the component fields and derive one deterministic, versioned text index. This is an agent
+lookup contract, not a native SQL composite-primary-key declaration.
+
+### Why not join composite-key fields with a hyphen?
+
+A raw separator can appear inside a component and create collisions. Encode every component first,
+use a separator excluded from the encoded component alphabet, fix component order, and version the
+format. Then agents in different sessions can reproduce the same lookup value safely.
diff --git a/apps/pages/content/blog/relationship-modeling-agent-datasets.md b/apps/pages/content/blog/relationship-modeling-agent-datasets.md
index 6a969d73..9a97eb25 100644
--- a/apps/pages/content/blog/relationship-modeling-agent-datasets.md
+++ b/apps/pages/content/blog/relationship-modeling-agent-datasets.md
@@ -123,6 +123,10 @@ the target value should be unique, required, stable, recognizable to the agent,
and present in exports or upstream systems. If the target value can change
during normal work, do not use it as the relationship handle.
+If the target is unique only by a tuple such as `(workspace_id, sku)`, define that identity before
+adding links. The [composite-primary-key guide](/blog/composite-primary-key-ai-agents) provides a
+versioned encoding contract for carrying the complete tuple through one Rowset index value.
+
Bad relationship keys usually look convenient:
- `name`, because names change and collide