Admin Setup Guide
Complete guide for administrators to create and configure semantic domains
Navigating to Domains
- Log in to Semaphor
- Select your project from the project list
- Click on "Domains" tab in the top navigation
Creating a Domain
Step 1: Create a New Domain
- On the Domains page, click the "Create Domain" button
- Fill in the domain details:
- Name: A unique identifier (e.g.,
sales_analytics) - Label: User-friendly display name (e.g.,
Sales Analytics) - Description: Brief explanation of what this domain covers
- Connection: Select the data connection this domain uses
- Name: A unique identifier (e.g.,
- Click "Create" to save your domain
Quick Setup Summary
Before diving into the details, here's the complete workflow at a glance:
- Create Domain → Name it and select a connection
- Add Datasets → Select tables, let AI generate labels (30-60s per dataset)
- Discover Relationships → Run AI discovery, accept suggestions (1-2 minutes)
- Save & Test → Save domain, test in a dashboard card
Total time: 5-10 minutes for a typical domain with 4-5 datasets
The AI does most of the work! Semaphor automatically generates field labels, descriptions, and discovers relationships between your datasets. Your main job is reviewing and accepting suggestions.
Now let's walk through each step in detail.
Step 2: Open the Domain Editor
After creating your domain, you'll be taken to the Domain Editor, which has three tabs:
- Datasets: Add and configure your data sources, fields, metrics, and context
- Relationships: Define how datasets connect
- Template: View and edit the domain as YAML/JSON
Adding Datasets
After creating your domain, you'll add the datasets (tables) you want users to explore.
Step 1: Select Your Datasets
- In the Domain Editor, ensure you're on the Datasets tab
- Click the "Add Dataset" button
- Choose one of three modes: Tables (default, multi-select physical tables), Data Model (reference an existing data model), or SQL Dataset (author a SQL-backed virtual dataset). See Virtual Datasets for the SQL Dataset walkthrough.
- Select the tables or views you want to include from your database
- Click "Add" to add each dataset
AI-Powered Labels: After adding datasets, Semaphor automatically generates user-friendly labels, field names, and descriptions using AI. This process takes a few moments but saves you from manually configuring each field.
Step 2: Wait for AI Processing
After you add datasets and click Save, Semaphor's AI automatically:
- Generates customer-facing labels for table names (e.g.,
orders_tbl→ "Orders") - Creates readable field names (e.g.,
cust_id→ "Customer ID") - Adds helpful descriptions for fields
- Classifies fields as identifiers (keys), facts (quantities to aggregate), or dimensions (attributes to group by)
- Suggests appropriate default aggregates for facts (SUM, COUNT, AVG)
Note: This AI operation typically takes 30-60 seconds per dataset. You'll see a loading indicator while processing.

Step 3: Review AI-Generated Fields (Optional)
After AI processing completes, you can review and adjust the generated labels:
- Click the "Edit" button (pencil icon) next to a dataset
- Review the AI-generated field labels and descriptions
- Make adjustments if needed (most users find the AI labels work well as-is)
- Hide any fields that shouldn't be visible to end users
Advanced: You can also create virtual datasets using custom SQL queries. See the Advanced Configuration section below.
That's all you need to get your datasets ready! The AI handles the labeling and classification automatically. Next, you'll define how these datasets relate to each other.
Creating Relationships
Relationships define how datasets connect to each other, enabling automatic joins when users explore data across multiple datasets.
Recommended Workflow: Use AI Discovery to automatically identify relationships. Most users find they don't need to manually define any relationships after using AI discovery.
Using AI-Powered Relationship Discovery
Semaphor can automatically discover potential relationships between your datasets using AI analysis of field names, data types, and semantic patterns. This is the fastest way to set up your domain.
The relationship discovery process typically takes 1-2 minutes to complete.
The entire process takes just a few steps:
- Click "Discover Relationships" in the Relationships tab
- Click "Run Discovery" and wait 1-2 minutes while AI analyzes your datasets
- Review the suggestions - each shows confidence (High/Medium/Low) and rationale
- Select relationships to add - use "Select High Confidence Only" or review each one
- Click "Accept Selected" - done!

What the AI analyzes:
- Field names (e.g.,
customer_idlikely joins tocustomers.id) - Data types (matching types for join keys)
- Semantic patterns (fact tables to dimension tables)
- Table structures (primary/foreign key patterns)
What you'll see in suggestions:
- From → To: Which datasets connect, many side first (e.g.,
orders → customers) - Join Fields: Which fields match (e.g.,
orders.customer_id = customers.id) - Confidence: High, Medium, or Low
- Rationale: Why the AI thinks this relationship exists
- Cardinality: Many-to-one, one-to-one, or many-to-many
Tip: Start by accepting only High Confidence relationships. You can always run discovery again or manually add relationships later.
That's it! After accepting relationships, your domain is ready for users to query. The relationships enable automatic joins when users select fields from multiple datasets.
Advanced: Manual Relationship Management
Note: Most users don't need to manually create relationships after using AI Discovery. This section is for advanced use cases.
If you need to manually add or edit a relationship:
- In the Relationships tab, click "Add Relationship"
- Fill in the relationship details:
- Name: Identifier (e.g.,
orders_to_customers) - From dataset: The many side, such as
orders - To dataset: The side whose fields identify one row, such as
customers - Match field: Which fields to match on each side
- Cardinality: Many-to-one, one-to-one, or many-to-many
- Default join: INNER, LEFT, RIGHT, or FULL
- Name: Identifier (e.g.,
- Check Enable automatic join resolution to allow automatic joins
- Click "Save"
If the To fields aren't a key of the To dataset, the save shows a warning: joins through the relationship may repeat rows. The Relationships tab shows the same warning under the relationship. If those fields do identify one row, click Mark ... as a unique key to declare it. See Join Keys.
When to manually add relationships:
- AI didn't detect a relationship you know exists
- You need to specify a specific join type (e.g., INNER vs LEFT)
- You're working with non-standard naming conventions
- You need composite key relationships (multiple join fields)
Testing Your Domain
Step 1: Save Your Changes
After configuring datasets and relationships:
- Click the "Save" button in the top-right corner
- Wait for the confirmation message
- Your changes are now persisted
Step 2: Review Validation
When you save, Semaphor validates the domain:
- Errors block the save and say what to change (for example, an aggregate placed in Facts)
- Warnings don't block the save (for example, a relationship whose To fields don't identify one row)
Step 3: Test in a Dashboard
- Create a new dashboard or open an existing one
- Add a new card
- Select "Semantic Domain" as the data source
- Choose your domain from the dropdown
- Try selecting fields from different datasets
- If relationships are configured correctly, joins happen automatically
- If fields from unrelated datasets are selected, you'll see an error
Advanced Configuration
Advanced Dataset Configuration
Creating Virtual Datasets
Virtual datasets are SQL-backed semantic datasets stored directly in your domain template. Use them for pre-aggregated data, reshaping, or complex joins encoded once and reused across dashboards.
In Add Dataset, switch to the SQL Dataset mode (alongside Tables and Data Model) to pick an owning connection, write SQL in a Monaco editor, run Preview Columns to discover fields, and save. Both subqueries and CTEs are supported.
See Virtual Datasets for the full walkthrough, including SQL examples, editing flow, maintenance actions (Preview Columns, Fetch New Columns, Sync Data Types), and how to handle drift.
Manual Field Configuration
If you need to manually adjust field properties beyond what AI generates:
- Click "Edit" on a dataset
- For each field, you can modify:
- Label: User-friendly display name
- Description: Help text explaining the field
- Data Type: String, Number, Date, DateTime, Boolean, and more (the database's exact type is kept alongside)
- Role: Identifier (a key), Fact (aggregatable), or Dimension (groupable)
- Default Aggregate: SUM, AVG, COUNT, MIN, MAX, and more, for facts
- Context: Instructions, other names, and example questions for agents
- Visibility: Show/hide from users (eye icon)
Best practices:
- Use clear labels: "Order Date" instead of "ord_dt"
- Add descriptions to help users understand fields
- Mark dimensions correctly for proper grouping
- Set default aggregates for facts
- Add context with the names your users actually say (for example, "sales" for revenue)
Domain Template Schema
Every semantic domain is stored as a template that defines datasets, relationships, metrics, and context. Understanding the schema helps with advanced configuration, version control, and CI/CD pipelines.
For a step-by-step tutorial on writing templates by hand, including how they translate into SQL queries, see the Template Guide.
Template Structure
schemaVersion: "2.1"
aiContext: {...} # Context for agents (optional)
datasets: [...] # Datasets with identifiers, facts, and dimensions
relationships: [...] # How datasets join
metrics: [...] # Dataset metrics and composed metrics
composedDimensions: [...] # Domain-level row expressions (optional)Templates saved in the 2.0 shape
Domains saved before version 2.1 keep working. They read as 2.1, and they are stored as 2.1 the next time you save. In the editor, Review upgrade lists what moves (for example, columns to Facts), any relationships to check, and can read each field's exact database type before you save. The API still accepts 2.0 bodies and upgrades them on save.
Dataset Schema
Each dataset in the datasets array has this structure:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for the dataset |
label | string | Yes | User-friendly display name |
description | string | No | Help text for users |
aiContext | string | object | No | Context for agents |
source | object | Yes | Where rows come from: kind (table or sql), connectionId, connectionType, and schema/table or sql |
primaryKey | string[] | No | Field names that identify one row |
uniqueKeys | string[][] | No | Other field-name lists that identify one row |
fields | object | Yes | Field definitions (see below) |
Fields Object:
fields:
identifiers: # Primary/foreign key fields
- name: id
label: "Order ID"
dataType: number
sourceField: id
type: primary
facts: # Row-level quantities to aggregate
- name: amount
label: "Amount"
dataType: number
sourceField: amount
defaultAggregate: SUM
dimensions: # Attributes to group and filter by
- name: order_date
label: "Order Date"
dataType: date
sourceField: order_date
granularity: dayRelationship Schema
Each relationship in the relationships array:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier |
from | { dataset, fields } | Yes | The many side |
to | { dataset, fields } | Yes | The side whose fields identify one row |
cardinality | enum | Yes | many_to_one, one_to_one, many_to_many |
defaultJoinType | enum | Yes | INNER, LEFT, RIGHT, FULL |
isAutoJoin | boolean | Yes | Enable automatic join resolution |
discoveredBy | enum | Yes | user, auto, fk_constraint, ai |
isActive | boolean | Yes | Whether the relationship is active |
Metric Schema
Metrics are stored in one metrics list. A dataset metric aggregates one dataset; a composed metric combines named inputs:
metrics:
- name: profit_margin
dataset: orders
label: "Profit Margin"
description: "Profit as percentage of revenue"
expression: "(SUM(amount) - SUM(cost)) / NULLIF(SUM(amount), 0)"
dataType: number
format:
type: percentage
decimals: 2
tags:
- finance
- kpi
- name: net_revenue
label: "Net Revenue"
expression: "{sales} - COALESCE({returned}, 0)"
inputs:
sales: { dataset: orders, field: amount, aggregate: SUM }
returned: { dataset: returns, field: amount, aggregate: SUM }| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Metric name |
dataset | string | Dataset metrics | The dataset the metric aggregates |
inputs | object | Composed metrics | Input references, each naming its dataset |
label | string | No | Display name |
description | string | No | Help text |
aiContext | string | object | No | Context for agents |
expression | string | Yes | The calculation |
dataType | enum | Dataset metrics | Logical type, usually number |
format | object | No | Display formatting options |
tags | string[] | No | Categorization tags |
Composed Dimension Schema
Domain-level row expressions are stored in composedDimensions. New groupings usually belong on a dataset as a dimension with an expression; composed dimensions keep working for existing domains and for grain mappings.
composedDimensions:
- name: order_month
label: "Order Month"
description: "Monthly time grouping"
expression: "DATE_TRUNC('month', {order_date})"
inputs:
order_date:
dataset: orders
field: order_date
dataType: date| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Dimension name |
label | string | Yes | Display name |
description | string | No | Help text |
expression | string | Yes | SQL expression with {input} tokens |
inputs | object | Yes | Input field references |
dataType | enum | No | string, number, boolean, date, datetime |
Direct Template Editing
Power users can edit the domain template directly as YAML or JSON. This is useful for:
- Bulk edits: Rename multiple fields at once
- Version control: Store templates in Git for change tracking
- CI/CD pipelines: Automate domain updates as part of deployments
- Migration: Copy configurations between environments
For the complete field-by-field tutorial, see the Template Guide. For programmatic template generation (AI agents, scripts), see the Agent Guide.
To manage templates via the REST API, see the Domain Templates API.
Opening the Template Tab
- Open your domain in the Domain Editor
- Click the Template tab
- View and edit the full template as YAML or JSON
- Click Save to apply changes
Template edits are validated when you save, the same way as edits in the other tabs. Errors block the save; warnings, such as a relationship that doesn't join on a key, are shown but don't block it. Test in a dashboard after changing relationships and metrics.
Complete Template Example
Here's a full domain template for a sales analytics domain:
schemaVersion: "2.1"
aiContext:
instructions: "Sales analytics for the online store. Amounts are in USD."
examples:
- "What was average order value by customer segment last quarter?"
datasets:
- name: orders
label: "Orders"
description: "Customer order transactions"
source:
kind: table
connectionId: "conn_abc123"
connectionType: PostgreSQL
schema: public
table: orders
primaryKey:
- id
fields:
identifiers:
- name: id
label: "Order ID"
dataType: number
sourceField: id
type: primary
- name: customer_id
label: "Customer ID"
dataType: number
sourceField: customer_id
type: foreign
dimensions:
- name: order_date
label: "Order Date"
dataType: date
sourceField: order_date
granularity: day
- name: status
label: "Status"
dataType: string
sourceField: status
- name: order_size_bucket
label: "Order Size"
description: "Categorizes orders by amount"
dataType: string
sourceField: amount
expression: |
CASE
WHEN amount > 1000 THEN 'Large'
WHEN amount > 100 THEN 'Medium'
ELSE 'Small'
END
- name: fiscal_quarter
label: "Fiscal Quarter"
description: "Fiscal quarter based on order date"
dataType: string
expression: "CONCAT('Q', EXTRACT(QUARTER FROM order_date), ' ', EXTRACT(YEAR FROM order_date))"
facts:
- name: amount
label: "Amount"
dataType: number
sourceField: amount
defaultAggregate: SUM
- name: cost
label: "Cost"
dataType: number
sourceField: cost
defaultAggregate: SUM
- name: quantity
label: "Quantity"
dataType: number
sourceField: quantity
defaultAggregate: SUM
- name: customers
label: "Customers"
source:
kind: table
connectionId: "conn_abc123"
connectionType: PostgreSQL
schema: public
table: customers
primaryKey:
- id
fields:
identifiers:
- name: id
label: "Customer ID"
dataType: number
sourceField: id
type: primary
dimensions:
- name: segment
label: "Customer Segment"
dataType: string
sourceField: segment
- name: region
label: "Region"
dataType: string
sourceField: region
relationships:
- name: orders_to_customers
from:
dataset: orders
fields:
- customer_id
to:
dataset: customers
fields:
- id
cardinality: many_to_one
defaultJoinType: LEFT
isAutoJoin: true
discoveredBy: ai
confidence: high
isActive: true
metrics:
- name: average_order_value
dataset: orders
label: "Average Order Value"
description: "Average revenue per order"
expression: "SUM(amount) / NULLIF(COUNT(id), 0)"
dataType: number
format:
type: currency
decimals: 2
currency: USD
tags:
- kpi
- sales
aiContext:
synonyms: [AOV, basket size]
- name: profit_margin
dataset: orders
label: "Profit Margin %"
description: "Profit as percentage of revenue"
expression: "(SUM(amount) - SUM(cost)) / NULLIF(SUM(amount), 0) * 100"
dataType: number
format:
type: percentage
decimals: 1
tags:
- kpi
- financeDomain-Level Calculated Fields
Add reusable metrics and calculated dimensions directly to your domain template. These fields become available to all users across all dashboards that use the domain.
Adding Metrics
Define metrics in the metrics list of your template. A metric that combines datasets uses named inputs:
metrics:
- name: conversion_rate
label: "Conversion Rate"
description: "Percentage of visitors who made a purchase"
expression: "{purchases} / NULLIF({visitors}, 0) * 100"
inputs:
purchases:
dataset: orders
field: id
aggregate: COUNT
visitors:
dataset: sessions
field: visitor_id
aggregate: DISTINCT
format:
type: percentage
decimals: 2
tags:
- marketing
- kpi
aiContext:
instructions: "Use for questions about how well traffic converts to orders."Adding Calculated Dimensions
Add a calculated dimension on its dataset, as a dimension with an expression:
datasets:
- name: customers
fields:
dimensions:
- name: customer_lifetime_tier
label: "Customer Lifetime Tier"
description: "Customer value tier based on total spend"
dataType: string
sourceField: total_spend
expression: |
CASE
WHEN total_spend > 10000 THEN 'Platinum'
WHEN total_spend > 5000 THEN 'Gold'
WHEN total_spend > 1000 THEN 'Silver'
ELSE 'Bronze'
ENDBest Practices for Domain Fields
- Document thoroughly: Include descriptions for every field. Users rely on this context.
- Use consistent naming: Follow a pattern like
metric_nameordimension_category. - Add tags: Group related fields (e.g.,
finance,marketing,operations). - Handle edge cases: Use
NULLIFfor division,COALESCEfor null values. - Test before publishing: Validate expressions with sample data.
Domain-level calculated fields appear in the Explorer with a Domain badge, distinguishing them from card-scoped or dashboard-scoped fields.
Next Steps
Once your semantic domain is configured:
- Train your users: Share the User Guide with end users
- Monitor usage: Track which relationships are used most frequently
- Gather feedback: Ask users which fields or relationships they need
- Optimize performance: Review query logs for slow multi-hop joins
- Expand gradually: Add new datasets and relationships based on user needs
- Automate with the API: Use the Semantic Domains API and Domain Templates API to manage domains programmatically