Semaphor

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.

POST
/api/v1/domains/{domainId}/template/discovery

Authorization

BearerAuth
AuthorizationBearer <token>

A project-scoped access token. Generate one via the Token API with your projectId and projectSecret.

In: header

Path Parameters

domainId*string

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": {}
    }
  ]
}