posthog/warehouse-source-new-version
Add support for a new vendor API version to an existing Data warehouse import source, or deprecate an old one. Use when a vendor ships a new API version (Stripe date versions, Shopify quarterly versions, header-pinned revisions, /vN/ URL bumps), when implementing a version-update or deprecation task for a source under products/warehouse_sources/backend/temporal/data_imports/sources, or when repinning an ExternalDataSource to a different version. Covers deciding whether a newly announced version needs supporting at all, version declaration, dispatch, pinning semantics, deprecation metadata, and migration scripts.
npx skills add https://github.com/PostHog/posthog --skill warehouse-source-new-version
Use this skill when a vendor has released a new API version and an existing source under
products/warehouse_sources/backend/temporal/data_imports/sources/<dir>/ must support it
while keeping every previously supported version functional.
_BaseSource in sources/common/base.py) declares:supported_versions: tuple[str, ...] — opaque vendor labels, never parsed or ordered by the framework. Default ("v1",) (UNVERSIONED_API_VERSION) for vendors without meaningful versioning.default_version: str — used when a source instance has no pin, and stamped onto newly created sources.api_docs_url: str | None — the vendor's API docs/changelog page (where new versions are announced). Distinct from docsUrl (posthog.com).deprecated_versions: tuple[VersionDeprecation, ...] — versions the vendor has deprecated (VersionDeprecation(version=..., sunset_at=date | None) from sources/common/base.py).ExternalDataSource row pins one version in its api_version column (NULL resolves to default_version). A schema may additionally carry a user-managed override in ExternalDataSchema.api_version (set from the schema's configuration page; not available for webhook-sync schemas) which wins over the source pin for that schema only. The sync pipeline resolves override → pin → default in workflow_activities/import_data_sync.py and hands the result to the source as SourceInputs.api_version — already resolved, never None there.api_version: str | None = None parameter carrying the source instance's resolved pin (None → default_version): get_schemas, validate_credentials, get_endpoint_permissions, and the WebhookSource management methods (create_webhook, sync_webhook_events, webhook_inputs_updated, get_external_webhook_info, delete_webhook). Callers with a source row (creation, refresh_schemas, background sync_new_schemas, webhook endpoints, schema-scoped probes) pass the resolved pin; pre-creation flows (wizard database_schema, one-shot setup) omit it, which resolves to default_version — the version the new row is stamped with (get_endpoint_permissions currently has only the pre-creation caller, so its parameter is always None today). Base-path/URL/header construction from it happens inside each source. Deliberately NOT version-threaded (pure mappings or version-independent surfaces — thread them if a real vendor version ever diverges there): get_desired_webhook_events/webhook_resource_map (event-name mappings), get_connection_metadata, and GitHub's per-repo webhook helpers in github_warehouse_repos.py.GET /api/public_source_configs/ (versions, defaultVersion, apiDocsUrl, deprecatedVersions) and per-instance via the source API (api_version, api_version_deprecation). The api_version pin is queryable in HogQL via the data_warehouse_sources system table.sources/tests/test_source_versions.py: default in supported and always the last entry (declare supported_versions oldest→newest; flip the default in the same PR), deprecated ⊆ supported, default never deprecated, https api_docs_url.Spotting a new vendor label is not a reason to support it. Before touching any source file, diff the new version against the one it supersedes — the source's current default_version, not every entry in supported_versions — from the vendor's docs and changelog, area by area:
WebhookSourceIf none of that differs for what this source reads, don't add the version. Leave supported_versions and default_version untouched and close the task with the per-area, changelog-cited evidence that the new label is indistinguishable from the default here. An extra label buys nothing and costs: a pin users can select, a version the tests, API, and UI carry forever, and the implied claim that the framework dispatches on it.
Add it when any of these hold:
deprecated_versions in the same PR;"Nothing changed" needs the same docs evidence as a divergence. An unread changelog is not a clean diff.
api_docs_url) and list what changed between the currently supported version(s) and the new one: renamed/removed fields, changed pagination, new required headers, changed webhook payloads. Verification is docs-only — there are no stored credentials and no live-sync harness, so the docs are the sole source of truth for what each version serves. This is also the evidence the gate above runs on.supported_versions and flip default_version to it — new sources always start on the newest stable version. A pinned row's sync path is unaffected by a default flip (that is the point of pinning), but two things still follow the new default: discovery/get_schemas if the pin isn't threaded there (step 3), and any row whose api_version is NULL. Reference the request layer's version constants instead of duplicating string literals.SourceInputs.api_version at the request layer:StripeSource.source_for_pipeline passes self.resolve_api_version(inputs.api_version) → stripe_source(...) → StripeClient(stripe_version=...)). Resolve through resolve_api_version at the source class — never hardcode a fallback version in the request layer.get_rows receives the resolved pin in inputs.api_version; credential fields can key off default_version.api_version param no caller varies, or a version→URL map with identical values, is a review finding, not forward-compat. Declaration-only (supported_versions/default_version and nothing else) is the correct shape just when the gate above passed on a non-wire reason — the old label is being retired, or the vendor switches behavior account-side rather than per request. If the gate passed on nothing at all, there is no PR.api_version parameter of get_schemas, validate_credentials, get_endpoint_permissions, and the webhook management methods. A multi-version source MUST build its discovery/probe/webhook clients from that parameter, not from default_version or a hardcoded header — otherwise a pinned source discovers/reconciles under the wrong version and its tables can disappear, duplicate, or fail reconciliation. Resolve it with self.resolve_api_version(api_version) — callers with a row pass an already-resolved value (mirroring SourceInputs.api_version), so the source-side resolve only covers pre-creation calls that pass None. Ignoring the parameter is only correct when you can state why the version makes no difference to that path.external_table_definitions were built for specific versions. When adding a version whose response shapes differ, gate the canonical column hints to the versions they were built for and let newer versions auto-infer the schema from the data (a set of hint-compatible versions checked where hints are applied). For has_managed_hogql_schema=True sources this includes the read path: hogql_definition's canonical column mapping is version-blind, so renamed columns need the canonical schema/descriptions updated too.resolve_api_version contract (test_source_versions.py covers every source). When versions diverge, shape fixtures per version from the vendor docs — a v1-shaped mock under a v2 pin proves nothing.feat(warehouse_sources): support <vendor> API version <label> — the scope is always warehouse_sources (the product), never the source dir/vendor name.deprecated_versions with the vendor's announced sunset date (or sunset_at=None if none). Never deprecate default_version — flip the default to the new version in the same PR.ExternalDataSource rows (api_version column) from the deprecated version to the new one, plus any safe data/schema transforms. It must be idempotent and reviewable, and its reverse must be a no-op — repinned rows are indistinguishable from natively-created ones, so a blanket downgrade would clobber legitimate native pins. Where migration is lossy or unsafe — including when the new version needs credentials that can't be derived from the stored ones — do not script it: document the manual path in the PR. Do not execute migrations or backfills; humans review and run them.ExternalDataSchema.api_version overrides in migration scripts — they are user-managed by design. The schema-level deprecation warning covers them; the user migrates them from the schema's configuration page.source.resolve_api_version(pinned) honors a present pin verbatim — even one no longer declared — because silently moving a customer to another version is the failure mode this framework prevents. Empty string / NULL fall back to the source class's own default_version._create_external_data_source in products/warehouse_sources/backend/presentation/views/external_data_source.py) stamps default_version, and migration 0075_backfill_externaldatasource_api_version backfilled pre-existing rows — so most rows carry a concrete pin. But api_version is nullable and direct-ORM creation paths that bypass the stamping (e.g. seed_engineering_analytics.py, and any future seeder/backfill/script) can leave it NULL, and a NULL pin resolves to default_version — so it follows a flip. Don't blanket-claim "every row is pinned, so a flip is safe"; verify the actual pin state for the source, and if a NULL cohort can exist, either back it out (written-not-run migration) or confirm the versions are request-identical.ExternalDataSource.api_version (support runbook: "Updating a warehouse source to a new vendor API version" in the PostHog/runbooks repo)."2026-02-25.clover", "v21.0", "2022-06-28". Copy them exactly; never normalize, sort, or parse.WebhookSource, check whether webhook-created clients (created at source-setup time, not sync time) also need the version and whether existing webhook subscriptions must be updated.validate_credentials, permission probes) run at creation time with no row pin; they may use the default/legacy version. Changing them is optional per version bump — verify the vendor accepts the validation calls under the new version before switching them.get_rows hits the rest; when they diverge per version, the probe passes while every table 404s..get() fallthrough silently sends no version header (tracking "latest", the drift this framework prevents). Assert coverage or raise.ci:preflight blocks it. Check max_migration.txt and renumber.sync_new_schemas, refresh_schemas, bulk sync-defaults). A schema-level api_version override on a version whose table set differs from the source's version can be disabled/soft-deleted by that diff — keep overrides to short verification windows, not as a long-term way to hold one table on another version.api_version parameter, add the vendor's version-rejection error signature to get_non_retryable_errors — otherwise a retired pin turns the ~6h discovery cadence into a permanent retry/error loop with no user-facing surface.validate_credentials enforces the pair that version needs — form-level required can't express "depends on the pin".The default outcome of a PR is that this skill does not change. Edit it only for a learning that clears all three bars: it generalizes across sources, it would change what a future agent does, and it is not already stated or derivable from the sections above. Vendor changelog details, per-source dispatch chains or code paths, and test specifics never qualify — that context lives in your PR, not here.
When something clears the bar, fold it into the section where an agent would need it (the gate, a step, a pitfall) as one vendor-neutral line. Do not append a learnings list, changelog, or dated notes anywhere in this file.
Take posthog/warehouse-source-new-version from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.