Queries
Use metric, records, matrix, analysis, input-options, and SQL queries.
Data App queries are server-side analytics specs. They describe the question you want answered; Semaphor compiles and executes the query through governed analytics.
Query Types
| Builder | Use it for | Returns |
|---|---|---|
semaphor.metric | KPIs and single-number summaries. | value, measures, comparison fields |
semaphor.records | Charts, tables, grouped breakdowns, trends. | records, columns, pagination |
semaphor.matrix | Pivot tables and cross-tabs. | matrixResult, grid |
semaphor.analysis | Driver analysis and analytical explanations. | records, drivers, summary fields |
semaphor.inputOptions | Dropdown/search option lists. | options |
semaphor.sql | Explicit SQL escape hatch. | records, columns, output metadata |
Prefer semantic query types first. Use SQL only when the request cannot be represented by the governed builders or the user explicitly asks for SQL.
Metric Query
Use metric for KPIs.
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...</div>;
if (result.error) return <div>{result.error.message}</div>;
return (
<div>
<div>{result.value}</div>
<div>{result.deltaPercent}% vs previous period</div>
</div>
);
}Use primaryMeasure when a query includes more than one measure and the UI needs to know which one is the hero value.
Metric comparison fields such as comparisonValue, delta, and deltaPercent
describe the query's primaryMeasure. If a dashboard shows several headline
KPI cards and each card needs its own period-over-period comparison, use
separate single-measure semaphor.metric queries rather than one multi-measure
query.
Records Query
Use records for chart series, grouped breakdowns, and tables.
const revenueByCampaign = semaphor.records({
id: 'revenue_by_campaign',
label: 'Revenue by Campaign',
source: orders,
fields: [campaignName, revenue],
orderBy: {
field: revenue,
direction: 'desc',
},
limit: 10,
});This produces a grouped query similar to:
select
dim_campaign.campaign_name,
sum(fact_orders.revenue) as revenue
from fact_orders
left join dim_campaign
on fact_orders.campaign_id = dim_campaign.campaign_id
group by dim_campaign.campaign_name
order by revenue desc
limit 10Your app did not write this SQL. The source-bearing fields and semantic relationship model let Semaphor compile it.
Trend Query
Use a date field and timeGrain for trends.
const monthlyRevenue = semaphor.records({
id: 'monthly_revenue',
label: 'Monthly Revenue',
source: orders,
dateField: orderDate,
timeGrain: 'month',
fields: [orderDate, revenue],
orderBy: {
field: orderDate,
direction: 'asc',
},
limit: 24,
});Matrix Query
Use matrix for pivot tables. For example, campaign channel by month.
const revenuePivot = semaphor.matrix({
id: 'revenue_by_channel_month',
label: 'Revenue by Channel and Month',
source: orders,
rows: [campaignChannel],
columns: [
{
field: orderDate,
grain: 'month',
},
],
values: [
{
field: revenue,
aggregate: 'SUM',
},
],
totals: {
rows: true,
columns: true,
},
});Matrix axes can use related dimensions and dates when the semantic model proves the relationship.
Analysis Query
Use analysis when the app needs explanation-style analytics, such as period-change drivers.
const revenueDrivers = semaphor.analysis({
id: 'revenue_drivers',
label: 'Revenue Drivers',
source: orders,
measures: [revenue],
primaryMeasure: revenue,
dimensions: [campaignName],
dateField: orderDate,
analysis: {
kind: 'period_change',
},
driverMode: 'positive_and_negative',
});The result can include records, drivers, changes, summaries, and execution metadata. Row-shaped analysis payloads are normalized into result.resultSets, for example result.resultSets?.changes?.records or result.resultSets?.drivers?.records. Treat result.executionResult as the authoritative status, validation, coverage, and lineage path.
Input Options Query
Use inputOptions for dropdowns and searchable filters.
const campaignOptions = semaphor.inputOptions({
id: 'campaign_options',
inputId: 'campaign',
label: 'Campaign options',
source: campaign,
labelField: campaignName,
valueField: campaignId,
searchField: campaignName,
limit: 100,
});Option queries are also governed server-side queries. They should not load every possible value into the browser.
SQL Fallback Query
Use sql only as an explicit escape hatch.
const customWindow = semaphor.sql({
id: 'custom_window',
label: 'Custom Window Analysis',
source: semaphor.source.sql({
connectionId: 'conn_123',
dialect: 'postgres',
label: 'Analytics warehouse',
}),
sql: `
select
month,
revenue,
revenue - lag(revenue) over (order by month) as revenue_delta
from monthly_revenue
order by month
`,
rationale:
'Uses a custom window function that is not represented by semantic records yet.',
limit: 100,
});SQL should be bounded, permissioned, and documented. Normal KPIs, tables, trends, filters, joins, and pivots should use the semantic builders when possible.
Loading And Error States
Every visual should handle loading and errors.
function Insight() {
const result = useSemaphorQuery(revenueByCampaign);
if (result.isLoading && !result.isStale) return <Skeleton />;
if (result.error) return <ErrorMessage error={result.error} />;
if (result.records.length === 0) return <EmptyState />;
return (
<>
{result.isStale ? <RefreshingBadge /> : null}
{result.isPartial ? <PartialResultWarning result={result} /> : null}
<Chart rows={result.records} columns={result.columns} />
</>
);
}Common Mistakes
| Mistake | Better |
|---|---|
Authoring "metrics" in query specs. | Use measures and primaryMeasure. |
| Filtering rows in client code. | Pass inputs to useSemaphorQuery. |
| Joining ids to labels in client code. | Use source-bearing related fields. |
| Relying on display labels as row keys. | Use columns[].key. |
| Using SQL because joins are needed. | Use semantic relationships first. |