Canonical reference: this guide describes the extensibility contract as it is actually implemented. Read it in full before creating a single file.
Overview
What you get for free (the shared base)
Any module that respects the contract inherits the following without writing a single line inserver/core/:
What you must write (the cost of a new connector)
server/core/ may be modified (AD-2).
Choosing the landing kind
The manifest’smodule_kind field declares the shape of the data the module produces.
Decision table:
Step by step
Step 1 — manifest.json
File: server/modules/<name>/manifest.json
Reference schema: server/core/schemas/manifest.schema.json
Excerpt adapted from gsc (reference module):
name: strict kebab-case^[a-z0-9-]+$(Layer 1 fails otherwise).auth_type:"oauth2"|"api_key"|"none".module_kind:"kpi"|"context"(absent ="kpi"by default).canonical_metric_mapping: additive metrics are written"source": "canonical"(string); non-additive metrics use the object form withaggregation_ruleandnon_additive: true(e.g.average_positionin gsc). Never sum a non-additive metric directly (AD-4).quotais optional; its absence means unlimited budget (no circuit-breaker).widget_refis optional forcontextmodules.
Step 2 — connector.py: pull() and transform()
File: server/modules/<name>/connector.py
Two required public functions.
2a. pull() — fetch and land
stg_<name>.sql will consume and that fact_daily_kpi aggregates via UNION. Never store metrics as wide columns in the raw table — long format is the contract.
2b. transform() — manifest-driven (AD-2)
transform()must never contain a hardcoded source field name: always userename_mapderived from the manifest.- Non-additive metrics (
non_additive: true) are stored raw per row with their weight (e.g.impressions). Never aggregated here. - Ratio metrics (
ctr, conversion rate…) are never stored. They are computed at the semantic layer.
Step 3 — dbt staging (module-owned)
Files:server/modules/<name>/dbt/staging/stg_<name>_daily.sqlserver/modules/<name>/dbt/staging/schema.yml
model-paths convention: dbt discovers these files via the model-paths key of dbt_project.yml. The chosen option (external model-path) keeps the models inside the module folder, not in dbt/models/.
Adapted from the gsc staging (reference):
schema.yml (grain-unique test required, AI-05):
grain_unique test is MANDATORY (AI-05: its absence is a code-review failure).
Step 4 — reports/*.json
File: server/modules/<name>/reports/<id>.json
Each file is a report pack. Conformance (Layer 3b via test_reports.py) validates that the declared metrics and dimensions are known.
Full structure with R6 fields (metric_definitions, llm_commentary_guidelines):
server/core/schemas/flow.report.schema.json
Base packs (in reports/) do not yet include metric_definitions; those fields are added through a flow override (flows_upsert — see Step 9).
Step 5 — Conformance fixtures
Files:golden_pull.json — rows in the source API’s raw format. Do not pre-process:
expected_facts.json — the exact result of transform(golden_pull.json):
- Fields renamed by
canonical_metric_mapping/canonical_dimension_mapping. - DROP fields (ratios like
ctr) are absent. - Pass-through fields (
pull_id,connector,date, unmapped dimensions) stay unchanged.
Step 6 — Conformance: the 4-layer suite
Full command (all layers):
Layer 1 failing = fail-fast (layers 2-4 are skipped).
Error format:
Step 7 — Live-integration pass (AI-13)
Any module that introduces a new external API requires a live-integration pass before it can be marked done (AI-13 DoD). Phase 1 — Mocked tests with respx:Step 8 — Backfill into datastreams + mappings
Once the module is in place, datastreams and mappings are created automatically bybackfill_datastreams() in server/core/datastreams.py.
How it works:
- Reads active
app.connection_refrows from Postgres. - Cross-references the module manifest (via
core.main._loaded_modules). - Creates one row in
app.datastreamsper(connection_ref x report_profile). - Seeds
app.datastream_mappingsfrom the manifest’scanonical_metric_mapping+canonical_dimension_mapping.- String form:
source_field→target_field - Object form:
source_field→val["canonical"]
- String form:
datastream_mappings → datastreams). No extra code required.
Date normalization (Story 8.10 / Refinement R3): if your source delivers dates in a format or timezone other than ISO-8601/UTC, declare date_format and source_timezone in the datastream’s config:
server/core/schemas/flow.datastream.schema.json ($.config.date_format, $.config.source_timezone).
Step 9 (Optional) — Edit the flow through MCP (flows_upsert)
Once the module is live, you can modify a datastream or enrich a report directly through MCP — without touching the DB.
Modify an existing datastream:
flows_upsert guardrails (do not bypass):
validate_flow()is called before any DB access: an unknown field (e.g.token,access_token) is rejected immediately.connection_ref_idis an opaque reference — never a token (AD-3).- Every change is audited (
flow_updatedinaudit_log) with a before/after diff. - An idempotent upsert (empty diff) writes nothing and generates no audit.
Final checklist
Enforced by the conformance suite. Every box must be checked before merge.- 1.
manifest.json: validauth_typeenum, kebabname,report_profiles≥ 1, completecanonical_*_mapping, non-additive metrics in object form withaggregation_rule. - 2.
connector.py:pull()returns{pull_id, row_count, date_from, date_to}.transform()is manifest-driven (no hardcoded source field name — AD-2 grep gate). Token never stored or logged (AD-3).pull_idreceived from the caller, never minted here (AD-7). - 3. (Optional but recommended) dbt staging with
QUALIFYsupersede (AD-7) and agrain_uniquetest (AI-05 mandatory if the model is created). - 4. Mappings auto-seeded via
backfill_datastreams()from the manifest’scanonical_*_mapping— nothing to write in SQL manually. - 5.
tests/fixtures/golden_pull.json+expected_facts.jsonpresent and consistent withtransform(). - 6. Full conformance suite: all 4 layers pass.
- 7. Live-integration pass documented (AI-13 DoD) — evidence in the Dev Agent Record.
- 8.
uv run ruff check serverpasses (AI-03: ASCII-only stdout in Python scripts). - 9. Schema Change Checklist (CONTRIBUTING.md) walked if a persisted schema or wire format changes.
Known pitfalls (Epics 1-8 retros)
AD-2 — The grep gate on module names in core/
Module names (gsc, google-analytics…) must never appear in server/core/. CI has a gate that greps server/core/ for these strings. If your connector.py imports from core/ while passing its own name as a literal, the gate fires.
Correct workaround: use provider as a variable, not a hardcoded literal.
AI-03 — ASCII stdout required
Every Python script that writes to stdout must use ASCII characters only. Non-ASCII characters causeUnicodeEncodeError on some CI runners and Windows terminals. If you need Unicode, declare the encoding explicitly:
AD-4 — Non-additive metrics: no naive aggregation
Metrics declarednon_additive: true (e.g. average_position, CTR, conversion rate) must never be summed directly from fact_daily_kpi. They must be read from a semantic view (semantic_avg_position) that applies impression-weighted aggregation:
R3 — Per-datastream date normalization
Do not assume the source delivers ISO-8601/UTC dates. Explicitly declaredate_format and source_timezone in the datastream’s config (Story 8.10). A wrong date-format assumption causes silent duplicates at the grain level.
Grain-unique test (AI-05)
Any dbt model that materializes a mart must declare aunique test over its entire grain key in schema.yml. The absence of this test is a code-review failure (AI-05). Silent double-counting = wrong KPIs in the widget.
Ratio metrics: never store
CTR, conversion rate, bounce rate — these are ratios (numerator / denominator). They are never stored in the raw table (example:ctr is dropped in gsc/connector.py _DROP_FIELDS). They are computed at the semantic layer from separately-stored numerators and denominators.
AI-44 — Migration naming
If your connector needs a new migration ininfra/nango/migrations/, the name must follow NNN_description.sql with a 3-digit zero-padded numeric prefix. Never reuse or renumber an already-applied prefix.
