Agent Builder Guide
Guidance for AI agents and developers generating Semaphor Data Apps.
This guide is the canonical public SDK authoring guide for coding agents and developers who generate Data App code. It explains the build sequence that produces understandable, governed, and reviewable apps.
Guidance metadata:
| Field | Value |
|---|---|
| Data App SDK guidance version | 2026-06-26.1 |
Minimum react-semaphor version covered | 0.1.418 |
| Last updated | 2026-06-26 |
If an installed Agent Plugin contains bundled SDK examples that disagree with this page, prefer this page and the public react-semaphor/data-app-sdk TypeScript declarations. Bundled plugin examples are an offline fallback.
Build Sequence
Use this order:
- Discover the semantic domain, datasets, fields, relationships, and primary dates.
- Plan the app before writing UI code.
- Choose one query owner per visual.
- Generate and materialize the Semaphor Data App contract.
- Build one component per insight or card from the generated contract.
- Add filters, option queries, loading, empty, and error states.
- Validate the generated contract and the local app.
- For edits, inspect the current app state before planning a change.
- Publish only after the app works with the target token scope.
Plan Before Code
Before writing app code, produce a short plan:
Visual: Revenue KPI
Query kind: metric
Base source: fact_orders
Measures: revenue
Date field: order_date
Inputs: date_range, campaign
Relationship needs: campaign filter resolves fact_orders -> dim_campaign
Visual: Revenue by Campaign
Query kind: records
Base source: fact_orders
Fields: dim_campaign.campaign_name, fact_orders.revenue
Inputs: date_range, campaign
Relationship needs: joined projection through campaign relationshipIf the semantic model cannot support a visual, say that explicitly:
Unsupported: Revenue by customer segment
Reason: fact_orders has buyer, bill-to, and ship-to customer relationships.
Required decision: choose which customer role to use.Do not silently switch to SQL because a semantic relationship is missing or ambiguous. Ask for clarification or surface the modeling gap.
Generated Contract Structure
For generated Data Apps, do not hand-author the governed query files. Generate
and materialize the Data App contract first, then import from
src/semaphor/generated.
src/
semaphor/
generated/
contract.manifest.json
index.ts
inputs.ts
queries.ts
views.ts
components/
filters/
CampaignFilter.tsx
DateRangeFilter.tsx
insights/
RevenueKpi.tsx
RevenueByCampaign.tsx
OrdersTable.tsx
App.tsxThis makes the app easier to inspect:
contract.manifest.json: generated contract manifest and hashes.index.ts: generated public API for inputs, views, queries, and accessors.queries.ts: generated governed query builders.inputs.ts: generated filters, controls, and option-query bindings.views.ts: generated view metadata and presentation helpers.components/insights: rendering and visual behavior.
Use generated helpers instead of guessing runtime keys:
import {
appInputSpecs,
createInputHandleMap,
metricValuesForView,
queries,
queryOptionsForView,
rowsForView,
tableColumnsForView,
} from '@/semaphor/generated';
import {
useSemaphorInputs,
useSemaphorQuery,
} from 'react-semaphor/data-app-sdk';
const handles = useSemaphorInputs(appInputSpecs);
const inputHandles = createInputHandleMap(handles);
const kpi = useSemaphorQuery(
queries.keyMeasures(),
queryOptionsForView.keyMeasures(inputHandles),
);
const metrics = metricValuesForView.keyMeasures(kpi);
const table = useSemaphorQuery(
queries.topProducts({ page, pageSize }),
queryOptionsForView.topProducts(inputHandles),
);
const rows = rowsForView.topProducts(table);Query Ownership
Each visual should have its own query unless it is intentionally reusing a shared query result.
Good:
RevenueKpi.tsx -> semaphor.metric(revenue)
RevenueByCampaign.tsx -> semaphor.records(campaignName, revenue)
OrdersTable.tsx -> semaphor.records(order fields, pagination)Avoid:
One giant query returns all rows.
Client code filters and groups the data for every card.Prefer Governed Query Builders
Use this order:
semaphor.metricfor KPIs.semaphor.recordsfor charts, trends, and tables.semaphor.matrixfor pivots.semaphor.analysisfor driver analysis.semaphor.sqlonly when the user explicitly asks for SQL or the semantic builders cannot express the request.
When SQL is used, include a rationale and keep the query bounded.
Use canonical builder names and fields:
- metric and analysis specs use
measuresandprimaryMeasure, notmetricsorprimaryMetric; - period-change analysis uses
analysis: { kind: 'period_change', ... }, notanalysisMode; - metric comparison uses a structured object such as
comparison: { kind: 'previous_period' }; - metric comparison is query-level and applies to
primaryMeasure; use separate single-measure metric specs when each KPI card needs its own period-over-period delta; - semantic sources must include
domainIdanddatasetName; includedatasetIdwhen available, but do not omitdatasetName; - SQL sources use
semaphor.source.sql({ connectionId, dialect?, label? }).
Inputs And Filters
Generated apps should use server-side option queries:
const campaignOptions = semaphor.inputOptions({
id: 'campaign_options',
inputId: 'campaign',
source: campaign,
labelField: campaignName,
valueField: campaignId,
searchField: campaignName,
limit: 100,
});Pass input handles to query hooks:
const result = useSemaphorQuery(revenueByCampaign, {
inputs: [dateRangeInput, campaignInput],
});Do not fetch all data and filter in client code.
Generated option queries are independent unless the generated contract declares
an explicit dependency. Do not cross-bind unrelated filters. Use
dependencies: { mode: 'explicit', includeInputIds: [...] } only when the
relationship path has been proven. Do not require filterFieldRef for
semaphor.inputOptions(...); active filtering is modeled on the input/filter
binding, while option queries provide source, label, value, search,
population, dependencies, inputs, and limit.
Relationship-Aware Behavior
When a visual needs labels or filters from a related dataset:
- author the related field with its own
source; - keep the query base source at the fact grain;
- let Semaphor resolve the relationship;
- inspect relationship diagnostics.
Example:
const revenueByCampaign = semaphor.records({
source: orders,
fields: [campaignName, revenue],
});This means "aggregate orders revenue and display campaign name." It does not mean "join in client code."
UI Quality Checklist
Every generated visual should include:
- loading state;
- error state;
- empty state;
- bounded row limit or pagination;
- readable number formatting;
- table sorting and pagination when rendering large tables;
- total rows for numeric table measures when users expect totals;
- no overlapping text or broken responsive layout.
Observability During Development
When developing, it is useful to expose which query drives each visual. Keep it development-only.
Useful fields:
- view id;
- query kind;
- base source;
- fields;
- inputs;
- pagination;
- relationship diagnostics;
- generated SQL if returned and allowed.
Do not show tokens or secrets.
Result State And Rendering
Every useSemaphorQuery result includes query state. Handle these fields before rendering data:
status;isLoading;isStale;isEmpty;isPartial;isFiltered;error;executionResult.
During refetch, isStale can be true while the hook keeps the previous successful payload available. Do not hide usable stale data behind a loading skeleton unless there is no renderable payload.
For partial governed results, inspect result.executionResult and show a warning or repair action instead of treating the payload as a fully answered success.
Rows are keyed by columns[].key. Do not render with display labels or semantic names as object keys:
{result.columns?.map((column) => (
<td key={column.key}>{String(row[column.key] ?? '')}</td>
))}For generated Data Apps, prefer generated accessors instead of raw row objects:
const rows = rowsForView.ordersTable(result);
const metrics = metricValuesForView.keyMeasures(result);Metric results expose the generated primary value through result.primaryValue.
Use generated metricValuesForView helpers for generated KPI views; use
result.primaryValue for hand-authored single-measure metric queries. Do not
read result.value in new Data App code.
For semaphor.analysis, row-shaped payloads are normalized into named result sets:
const changes = result.resultSets?.changes?.records ?? [];
const drivers = result.resultSets?.drivers?.records ?? [];Treat result.executionResult as the authoritative governed result contract for status, validation, coverage, diagnostics, row counts, and lineage. Top-level analysis fields are display conveniences. Top-level fieldsUsed is compact display metadata and does not carry derivedField; full derived-field lineage belongs in executionResult.fieldsUsed.
Agent Recovery Rules
When Semaphor returns diagnostics:
| Diagnostic | Agent action |
|---|---|
relationshipDiagnostics.status: 'missing' | Ask to model the relationship, complete the required relationship metadata, add a valid option population, or remove the cross-source field. |
relationshipDiagnostics.status: 'ambiguous' | Ask which modeled relationship to use. |
fanout_risk | Avoid joined measure projection; use safe filtering or modeled aggregate semantics. |
missing_field | Re-read schema and choose a visible field. |
Do not paper over these diagnostics with prompt-only rules or guessed SQL.