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-12.1 |
Minimum react-semaphor version covered | 0.1.391 |
| Last updated | 2026-06-12 |
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.
- Author sources and fields in shared files.
- Build one component per insight or card.
- Add filters and option queries.
- Add loading, empty, and error states.
- Validate locally.
- 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.
File Structure
Prefer small files over one giant entrypoint. For the current React starter, this often means avoiding one giant App.tsx.
src/
data-app/
sources.ts
fields.ts
queries.ts
inputs.ts
components/
filters/
CampaignFilter.tsx
DateRangeFilter.tsx
insights/
RevenueKpi.tsx
RevenueByCampaign.tsx
OrdersTable.tsx
App.tsxThis makes the app easier to inspect:
sources.ts: dataset ownership.fields.ts: source-bearing field refs.queries.ts: Semaphor query builders.inputs.ts: filters and controls.components/insights: rendering and visual behavior.
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.
Normal cascading option queries may omit dependencies; the default behavior is auto. 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 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.