Preview AI relationship discovery
Analyzes the datasets in your template and suggests how they relate
to each other: for example, that orders.customer_id likely
references customers.id. This is a preview; nothing is saved.
Send a template with at least two datasets, in the same shape and
with the same section rules as PUT (2.1, or 2.0 upgraded).
Discovery inspects field names, types, and naming conventions.
Suggestions use from (the many side) and to (the side whose
fields identify one row). The response includes both the raw suggestions
and a candidateTemplate with those relationships already merged in,
ready to save or edit via
PUT /api/v1/domains/{domainId}/template.
Discovery is skipped when:
- The template has fewer than 2 datasets (nothing to relate).
- The template already has active relationships (to avoid overwriting your existing joins).
When skipped, skipped is true and skippedReason explains why.
When discovery runs, any existing inactive relationships are
preserved. New relationships are appended with
discoveredBy: "ai" and isActive: true. The confidence and
joinPriority fields reflect how certain discovery is about each
suggestion. candidateTemplate is in the 2.1 shape.
Authorization
BearerAuth A project-scoped access token. Generate one via the Token API with your projectId and projectSecret.
In: header
Path Parameters
The unique identifier of the semantic domain.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X post "https://semaphor.cloud/api/v1/domains/domain_abc123/template/discovery" \ -H "Content-Type: application/json" \ -d '{ "template": {} }'{
"suggestions": [
{
"id": "string",
"from": {
"dataset": "orders",
"fields": [
"customer_id"
]
},
"to": {
"dataset": "orders",
"fields": [
"customer_id"
]
},
"cardinality": "many_to_one",
"defaultJoinType": "LEFT",
"confidence": "high",
"rationale": "orders.customer_id likely references customers.id"
}
],
"candidateTemplate": {
"schemaVersion": "2.1",
"id": "domain_abc123",
"name": "sales_metrics",
"label": "Sales Metrics",
"description": "Revenue and order tracking",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"datasets": [
{
"name": "orders",
"label": "Orders",
"description": "string",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"analyticsRole": "fact",
"source": {
"kind": "table",
"connectionId": "conn_1",
"connectionType": "PostgreSQL",
"database": "analytics",
"schema": "public",
"table": "orders"
},
"primaryKey": [
"order_id"
],
"uniqueKeys": [
[
"order_number"
]
],
"primaryDateField": "order_date",
"fields": {
"identifiers": [
{
"name": "amount",
"label": "Amount",
"description": "string",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"dataType": "number",
"nativeDataType": "numeric(12,2)",
"sourceField": "amount",
"hidden": true,
"tags": [
"string"
],
"type": "primary"
}
],
"facts": [
{
"name": "amount",
"label": "Amount",
"description": "string",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"dataType": "number",
"nativeDataType": "numeric(12,2)",
"sourceField": "amount",
"hidden": true,
"tags": [
"string"
],
"expression": "amount_cents / 100",
"defaultAggregate": "SUM",
"filters": [
{
"field": "string",
"operator": "string",
"value": null
}
],
"format": null,
"unit": "USD",
"comparisonPolarity": "higher_is_better",
"isDefault": true
}
],
"dimensions": [
{
"name": "amount",
"label": "Amount",
"description": "string",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"dataType": "number",
"nativeDataType": "numeric(12,2)",
"sourceField": "amount",
"hidden": true,
"tags": [
"string"
],
"expression": "string",
"isDisplayField": true,
"granularity": "second",
"format": {
"type": "temporal_bucket",
"presentation": {
"mode": "auto"
}
}
}
]
}
}
],
"relationships": [
{
"id": "string",
"name": "orders_to_customers",
"description": "string",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"from": {
"dataset": "orders",
"fields": [
"customer_id"
]
},
"to": {
"dataset": "orders",
"fields": [
"customer_id"
]
},
"cardinality": "many_to_one",
"defaultJoinType": "LEFT",
"isAutoJoin": true,
"joinPriority": 10,
"isActive": true,
"discoveredBy": "user",
"confidence": "high"
}
],
"metrics": [
{
"name": "revenue",
"dataset": "orders",
"expression": "SUM(amount)",
"label": "Revenue",
"description": "string",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"dataType": "number",
"nativeDataType": "string",
"format": null,
"unit": "string",
"comparisonPolarity": "higher_is_better",
"isDefault": true,
"hidden": true,
"tags": [
"string"
]
}
],
"composedDimensions": [
{
"name": "order_month",
"label": "Order Month",
"description": "string",
"aiContext": "Revenue is net of refunds. Use order_date for time questions.",
"expression": "DATE_TRUNC('month', {order_date})",
"inputs": {
"order_date": {
"dataset": "orders",
"field": "order_date"
}
},
"dataType": "string",
"granularity": "second",
"format": {
"type": "temporal_bucket",
"presentation": {
"mode": "auto"
}
},
"grainMapping": {
"property1": "string",
"property2": "string"
}
}
],
"grainMappings": [
{
"sourceDimension": "order_month",
"targetDataset": "returns",
"targetColumn": "return_date"
}
]
},
"skipped": true,
"skippedReason": "insufficient_datasets",
"warnings": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
]
}{
"error": "string",
"details": {},
"errors": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
],
"warnings": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
]
}{
"error": "string",
"details": {},
"errors": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
],
"warnings": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
]
}{
"error": "string",
"details": {},
"errors": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
],
"warnings": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
]
}{
"error": "string",
"details": {},
"errors": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
],
"warnings": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
]
}{
"error": "string",
"details": {},
"errors": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
],
"warnings": [
{
"code": "row_expression_in_dataset_metric",
"message": "margin works row by row. Add it as a fact (to aggregate) or a dimension (to group by).",
"severity": "error",
"hint": "string",
"context": {}
}
]
}