Patterns
Reusable Data App patterns with business scenarios and code.
These patterns use the same commerce schema from the relationship guide. Adapt the domainId, datasetName, and field names to your Semaphor project.
The Generated SQL tabs show the kind of governed SQL Semaphor can compile from the SDK intent. They are illustrative: the exact SQL can vary by database dialect, security policy, relationship model, and optimizer strategy.
Each pattern is named by the technical concept it teaches. The business scenario underneath grounds that concept in a realistic user request, then the source-bearing fields and code show how to implement it.
| Concept | Business scenario |
|---|---|
| Metric query with period comparison | Revenue leader wants a KPI and previous-period change. |
| Joined projection | Marketing manager wants revenue by campaign name. |
| Joined input filtering a fact view | User picks campaign names to filter order-backed visuals. |
| Same-source cascading inputs | Region narrows State, and State narrows Region. |
| Server-paginated records table | Operations user inspects recent orders without loading every row. |
| Conformed dimension filter across facts | One Material Family filter updates purchase and sales views. |
| Shared date range across facts | One Date Range control filters purchase and sales views using each fact's date field. |
| Fact-bridged option population | Store selection narrows Product options through inventory. |
Shared Setup
import { useState } from 'react';
import {
SemaphorDataAppProvider,
semaphor,
useClearInvalidSemaphorInputValue,
useSemaphorInput,
useSemaphorQuery,
} from 'react-semaphor/data-app-sdk';
export const orders = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_orders',
});
export const campaign = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_campaign',
});
export const geo = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_geo',
});
export const store = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_store',
});
export const revenue = semaphor.field.measure('revenue', {
source: orders,
aggregate: 'SUM',
});
export const orderDate = semaphor.field.date('order_date', {
source: orders,
});
export const orderId = semaphor.field.id('order_id', {
source: orders,
});
export const campaignId = semaphor.field.id('campaign_id', {
source: campaign,
});
export const campaignName = semaphor.field.dimension('campaign_name', {
source: campaign,
});
export const storeId = semaphor.field.id('store_id', {
source: store,
});
export const storeName = semaphor.field.dimension('store_name', {
source: store,
});
export const purchaseLine = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_purchase_line',
});
export const salesLine = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_sales_line',
});
export const material = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_material',
});
export const materialFamily = semaphor.field.dimension('material_family', {
source: material,
});
export const purchaseDate = semaphor.field.date('purchase_date', {
source: purchaseLine,
});
export const saleDate = semaphor.field.date('sale_date', {
source: salesLine,
});
export const netPurchaseValue = semaphor.field.measure('net_purchase_value', {
source: purchaseLine,
aggregate: 'SUM',
});
export const salesValue = semaphor.field.measure('sales_value', {
source: salesLine,
aggregate: 'SUM',
});
function SelectInput({
input,
options,
}: {
input: { value: unknown; setValue(value: unknown): void };
options: Array<{ label: string; value: string | number | boolean }>;
}) {
return (
<select
value={String(input.value ?? '')}
onChange={(event) => input.setValue(event.currentTarget.value)}
>
<option value="">All</option>
{options.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</select>
);
}Metric Query With Period Comparison
Business scenario
A revenue leader wants a simple headline KPI that answers: "How much revenue did we make, and how does it compare with the previous period?" This is the kind of number that usually sits at the top of a dashboard.
Technical concept
Use a metric query when the visual is a single KPI. The query has one base source, one or more measures, and an optional comparison. When a query has multiple measures, comparison fields apply only to primaryMeasure; use separate single-measure metric queries when every KPI card needs its own period-over-period delta. Here, fact_orders is the base source and revenue is the measure Semaphor sums.
How it translates to code
source: orderstells Semaphor the KPI is based onfact_orders.measures: [revenue]tells Semaphor which numeric field to aggregate.primaryMeasure: revenuetells the UI which value is the hero number.comparison.kind = 'previous_period'asks Semaphor to calculate the previous-period delta server-side.
Source-bearing fields for this scenario:
const orders = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_orders',
});
const revenue = semaphor.field.measure('revenue', {
source: orders,
aggregate: 'SUM',
});
const orderDate = semaphor.field.date('order_date', {
source: orders,
});const revenueKpi = semaphor.metric({
id: 'revenue_kpi',
label: 'Revenue',
source: orders,
measures: [revenue],
primaryMeasure: revenue,
dateField: orderDate,
comparison: {
kind: 'previous_period',
},
});
function RevenueKpi() {
const result = useSemaphorQuery(revenueKpi);
if (result.isLoading) return <div>Loading revenue...</div>;
if (result.error) return <div>{result.error.message}</div>;
return (
<div>
<div>Revenue</div>
<strong>{String(result.value ?? '')}</strong>
<span>{result.deltaPercent ?? 0}%</span>
</div>
);
}Joined Projection
Business scenario
A marketing manager wants to see which campaigns are driving the most revenue. They do not want to see campaign ids like cmp_123; they want readable campaign names such as "Holiday Retargeting."
Technical concept
This is a joined projection. The measure comes from fact_orders, but the display dimension comes from dim_campaign. Semaphor uses the modeled relationship between orders and campaigns to group revenue by campaign name.
How it translates to code
source: orderskeeps the query grain at the orders fact table.fields: [campaignName, revenue]asks for a related dimension and a measure.orderBy: revenue descranks the campaigns by revenue.- The app does not join ids to labels in client code; Semaphor resolves the relationship server-side.
Source-bearing fields for this scenario:
const orders = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_orders',
});
const campaign = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_campaign',
});
const revenue = semaphor.field.measure('revenue', {
source: orders,
aggregate: 'SUM',
});
const campaignName = semaphor.field.dimension('campaign_name', {
source: campaign,
});const revenueByCampaign = semaphor.records({
id: 'revenue_by_campaign',
label: 'Revenue by Campaign',
source: orders,
fields: [campaignName, revenue],
orderBy: {
field: revenue,
direction: 'desc',
},
limit: 10,
});
function RevenueByCampaignList() {
const result = useSemaphorQuery(revenueByCampaign);
if (result.isLoading) return <div>Loading campaigns...</div>;
if (result.error) return <div>{result.error.message}</div>;
const [campaignColumn, revenueColumn] = result.columns || [];
return (
<ul>
{result.records.map((row, index) => (
<li key={index}>
{String(row[campaignColumn?.key || 'campaign_name'] ?? '')}:{' '}
{String(row[revenueColumn?.key || 'revenue'] ?? '')}
</li>
))}
</ul>
);
}Joined Input Filtering A Fact View
Business scenario
A user wants to pick one or more campaigns from a dropdown and have every campaign-backed visual update. The dropdown should show campaign names, but the selected value should be the stable campaign id.
Technical concept
This combines an inputOptions query with a filter input. The option query loads dropdown choices from dim_campaign. The filter input carries the selected values into a records query.
How it translates to code
labelField: campaignNameis what the user sees.valueField: campaignIdis what the input stores and sends to Semaphor.useSemaphorInputcreates the active filter state.- Passing
inputs: [campaignInput]touseSemaphorQuerymakes filtering server-side.
Source-bearing fields for this scenario:
const campaign = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_campaign',
});
const campaignName = semaphor.field.dimension('campaign_name', {
source: campaign,
});
const campaignId = semaphor.field.id('campaign_id', {
source: campaign,
});const campaignOptions = semaphor.inputOptions({
id: 'campaign_options',
inputId: 'campaign',
label: 'Campaign options',
source: campaign,
labelField: campaignName,
valueField: campaignId,
searchField: campaignName,
limit: 100,
});
function CampaignFilterAndChart() {
const campaignInput = useSemaphorInput({
id: 'campaign',
label: 'Campaign',
kind: 'filter',
field: campaignId,
operator: 'in',
multi: true,
});
const options = useSemaphorQuery(campaignOptions);
const result = useSemaphorQuery(revenueByCampaign, {
inputs: [campaignInput],
});
return (
<>
<select
multiple
value={(campaignInput.value as string[] | undefined) || []}
onChange={(event) => {
campaignInput.setValue(
Array.from(event.currentTarget.selectedOptions).map(
(option) => option.value,
),
);
}}
>
{options.options.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</select>
<pre>{JSON.stringify(result.records, null, 2)}</pre>
</>
);
}Same-Source Cascading Inputs
Business scenario
A sales leader wants geography filters. If they choose South for Region, the State dropdown should show only southern states. If they choose Texas, the Region dropdown can narrow to South. The filters should narrow option lists without automatically changing other input values.
Technical concept
This is same-source cascading. Region and State both live in dim_geo, so Semaphor can narrow each option query using the other active input. The default dependency behavior is auto, so the option queries can use compatible active inputs without authoring a custom dependency list.
How it translates to code
regionOptionsandstateOptionsboth querydim_geo.stateOptionsreceivesinputs: [regionInput], so selected Region narrows State options.regionOptionsreceivesinputs: [stateInput], so selected State narrows Region options.useClearInvalidSemaphorInputValueclears a stale value only after the narrowed option query succeeds.
Source-bearing fields for this scenario:
const geo = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_geo',
});
const regionId = semaphor.field.id('region_id', { source: geo });
const regionName = semaphor.field.dimension('region_name', {
source: geo,
});
const stateId = semaphor.field.id('state_id', { source: geo });
const stateName = semaphor.field.dimension('state_name', {
source: geo,
});const regionOptions = semaphor.inputOptions({
id: 'region_options',
inputId: 'region',
source: geo,
labelField: regionName,
valueField: regionId,
});
const stateOptions = semaphor.inputOptions({
id: 'state_options',
inputId: 'state',
source: geo,
labelField: stateName,
valueField: stateId,
});
function GeoFilters() {
const regionInput = useSemaphorInput({
id: 'region',
label: 'Region',
kind: 'filter',
field: regionId,
operator: 'in',
multi: true,
});
const stateInput = useSemaphorInput({
id: 'state',
label: 'State',
kind: 'filter',
field: stateId,
operator: 'in',
multi: true,
});
const regionOptionResult = useSemaphorQuery(regionOptions, {
inputs: [stateInput],
});
const stateOptionResult = useSemaphorQuery(stateOptions, {
inputs: [regionInput],
});
useClearInvalidSemaphorInputValue(regionInput, regionOptionResult);
useClearInvalidSemaphorInputValue(stateInput, stateOptionResult);
return (
<div>
<SelectInput input={regionInput} options={regionOptionResult.options} />
<SelectInput input={stateInput} options={stateOptionResult.options} />
</div>
);
}Server-Paginated Records Table
Business scenario
An operations user wants to inspect recent orders. There may be millions of orders, so the table must not load every row into the browser. Users should page through server-backed results and keep sorting consistent across the whole dataset.
Technical concept
Use a records query with pagination. The current page and page size are part of the Semaphor query spec, so the server returns only the requested slice. The table renders returned rows and uses pagination metadata to enable or disable navigation.
How it translates to code
pagination: { page, pageSize }asks Semaphor for one server page.orderBy: orderDate descsorts the full result set server-side.result.columnsgives stable column keys for rendering cells.result.pagination.hasPrevPageandhasNextPagedrive the page buttons.
Source-bearing fields for this scenario:
const orders = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_orders',
});
const campaign = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_campaign',
});
const orderId = semaphor.field.id('order_id', { source: orders });
const orderDate = semaphor.field.date('order_date', { source: orders });
const revenue = semaphor.field.measure('revenue', {
source: orders,
aggregate: 'SUM',
});
const campaignName = semaphor.field.dimension('campaign_name', {
source: campaign,
});function OrdersTable() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const query = semaphor.records({
id: 'orders_table',
label: 'Orders',
source: orders,
fields: [orderId, orderDate, campaignName, revenue],
orderBy: {
field: orderDate,
direction: 'desc',
},
pagination: {
page,
pageSize,
},
});
const result = useSemaphorQuery(query);
if (result.isLoading) return <div>Loading orders...</div>;
if (result.error) return <div>{result.error.message}</div>;
return (
<>
<table>
<thead>
<tr>
{result.columns?.map((column) => (
<th key={column.key}>{column.label || column.key}</th>
))}
</tr>
</thead>
<tbody>
{result.records.map((row, rowIndex) => (
<tr key={rowIndex}>
{result.columns?.map((column) => (
<td key={column.key}>{String(row[column.key] ?? '')}</td>
))}
</tr>
))}
</tbody>
</table>
<button
disabled={!result.pagination?.hasPrevPage}
onClick={() => setPage((current) => current - 1)}
>
Previous
</button>
<button
disabled={!result.pagination?.hasNextPage}
onClick={() => setPage((current) => current + 1)}
>
Next
</button>
</>
);
}Conformed Dimension Filter Across Facts
Business scenario
A merchandising leader wants one Material Family filter at the top of the app. When the user selects Metals, the purchase spend chart and the sales revenue chart should both update, even though one view reads from purchase lines and the other reads from sales lines.
Technical concept
This is a conformed dimension filter across facts. The visible input is authored once from dim_material.material_family, but each query binds that same input to the relationship path that connects its own fact table to dim_material.
How it translates to code
materialFamilyInputis the one visible input the user controls.- The purchase query uses
semaphor.bindInput(...)with the purchase-to-material relationship hint. - The sales query uses the same input value, but with the sales-to-material relationship hint.
- Semaphor proves each relationship independently, so a filter can apply to multiple fact-backed views without client-side joins.
Source-bearing fields for this scenario:
const purchaseLine = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_purchase_line',
});
const salesLine = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_sales_line',
});
const material = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_material',
});
const materialFamily = semaphor.field.dimension('material_family', {
source: material,
});
const netPurchaseValue = semaphor.field.measure('net_purchase_value', {
source: purchaseLine,
aggregate: 'SUM',
});
const salesValue = semaphor.field.measure('sales_value', {
source: salesLine,
aggregate: 'SUM',
});function MaterialFamilyDashboard() {
const materialFamilyInput = useSemaphorInput({
id: 'material_family',
label: 'Material Family',
kind: 'filter',
field: materialFamily,
operator: 'in',
multi: true,
});
const purchaseByMaterial = semaphor.records({
id: 'purchase_by_material',
label: 'Purchases by Material Family',
source: purchaseLine,
fields: [materialFamily, netPurchaseValue],
inputs: [
semaphor.bindInput(materialFamilyInput, {
field: materialFamily,
operator: 'in',
relationshipHint: {
relationshipIds: ['purchase_line_to_material'],
},
}),
],
limit: 10,
});
const salesByMaterial = semaphor.records({
id: 'sales_by_material',
label: 'Sales by Material Family',
source: salesLine,
fields: [materialFamily, salesValue],
inputs: [
semaphor.bindInput(materialFamilyInput, {
field: materialFamily,
operator: 'in',
relationshipHint: {
relationshipIds: ['sales_line_to_material'],
},
}),
],
limit: 10,
});
const purchaseResult = useSemaphorQuery(purchaseByMaterial);
const salesResult = useSemaphorQuery(salesByMaterial);
return (
<div>
<MaterialFamilySelect input={materialFamilyInput} />
<pre>{JSON.stringify(purchaseResult.records, null, 2)}</pre>
<pre>{JSON.stringify(salesResult.records, null, 2)}</pre>
</div>
);
}Shared Date Range Across Facts
Business scenario
An operations leader wants one Date Range control for the whole app. Purchases should filter by purchase date, while sales should filter by sale date. The user should not have to manage separate date controls for each view.
Technical concept
This is a shared date input with query-specific bindings. The visible input stores one date range, but each fact-backed query maps that same input to the date field that belongs to the query's source.
How it translates to code
dateRangeInputis the one visible input.- The purchase query binds it to
fact_purchase_line.purchase_date. - The sales query binds it to
fact_sales_line.sale_date. - This pattern is for simple event or fact dates. Snapshot/latest/as-of selectors need explicit modeled semantics before the app should treat them as equivalent.
Source-bearing fields for this scenario:
const purchaseDate = semaphor.field.date('purchase_date', {
source: purchaseLine,
});
const saleDate = semaphor.field.date('sale_date', {
source: salesLine,
});function PurchaseAndSalesTrends() {
const dateRangeInput = useSemaphorInput({
id: 'date_range',
label: 'Date Range',
kind: 'filter',
field: purchaseDate,
operator: 'between',
defaultValue: ['2026-01-01', '2026-01-31'],
});
const purchaseTrend = semaphor.records({
id: 'purchase_trend',
label: 'Purchase Trend',
source: purchaseLine,
fields: [purchaseDate, netPurchaseValue],
inputs: [
semaphor.bindInput(dateRangeInput, {
field: purchaseDate,
operator: 'between',
}),
],
orderBy: {
field: purchaseDate,
direction: 'asc',
},
limit: 100,
});
const salesTrend = semaphor.records({
id: 'sales_trend',
label: 'Sales Trend',
source: salesLine,
fields: [saleDate, salesValue],
inputs: [
semaphor.bindInput(dateRangeInput, {
field: saleDate,
operator: 'between',
}),
],
orderBy: {
field: saleDate,
direction: 'asc',
},
limit: 100,
});
const purchaseResult = useSemaphorQuery(purchaseTrend);
const salesResult = useSemaphorQuery(salesTrend);
return (
<div>
<DateRangePicker input={dateRangeInput} />
<pre>{JSON.stringify(purchaseResult.records, null, 2)}</pre>
<pre>{JSON.stringify(salesResult.records, null, 2)}</pre>
</div>
);
}Fact-Bridged Option Population
Business scenario
An inventory manager selects a store and wants the Product dropdown to show only products stocked in that store. The product list should not show every product in the catalog if the selected store has no inventory for it.
Technical concept
This is a fact-bridged option population. Store and Product are different dimensions, and they are not directly related. Inventory is the fact population that connects stores to products.
How it translates to code
source: productsays the dropdown choices are products.labelField: productNameandvalueField: productIddefine what the user sees and what gets stored.population.kind = 'related_population'tells Semaphor that option relevance comes from another source.baseSource: inventorytells Semaphor to usefact_inventory_snapshotas the population that connects Store to Product.- Passing
inputs: [storeInput]narrows the product options by the selected store.
Source-bearing fields for this scenario:
const inventory = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'fact_inventory_snapshot',
});
const product = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_product',
});
const store = semaphor.source.semantic({
domainId: 'commerce',
datasetName: 'dim_store',
});
const productId = semaphor.field.id('product_id', {
source: product,
});
const productName = semaphor.field.dimension('product_name', {
source: product,
});
const storeId = semaphor.field.id('store_id', {
source: store,
});const productOptions = semaphor.inputOptions({
id: 'product_options',
inputId: 'product',
source: product,
labelField: productName,
valueField: productId,
population: {
kind: 'related_population',
baseSource: inventory,
},
limit: 100,
});
const storeInput = useSemaphorInput({
id: 'store',
label: 'Store',
kind: 'filter',
field: storeId,
operator: 'in',
multi: true,
});
const productOptionResult = useSemaphorQuery(productOptions, {
inputs: [storeInput],
});Without related_population, Semaphor could return all products in the catalog. With it, Semaphor can return products relevant to the inventory population and active store filter.