Semaphor
Data Apps

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

BuilderUse it forReturns
semaphor.metricKPIs and single-number summaries.value, measures, comparison fields
semaphor.recordsCharts, tables, grouped breakdowns, trends.records, columns, pagination
semaphor.matrixPivot tables and cross-tabs.matrixResult, grid
semaphor.analysisDriver analysis and analytical explanations.records, drivers, summary fields
semaphor.inputOptionsDropdown/search option lists.options
semaphor.sqlExplicit 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 10

Your 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

MistakeBetter
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.

On this page