/upgrade-v3
SkillDev toolsUpgrade a connector repo from application-sdk v2 to v3 — runs the import rewriter, performs AI-assisted structural refactoring, and validates the result with the upgrade checker.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the /upgrade-v3 skill
What this skill tells your AI
The instructions your AI receives, as published by atlanhq/application-sdk in .claude/skills/upgrade-v3/SKILL.md and read by ahel’s review.
Performs a complete v2 → v3 upgrade of an application-sdk connector.
Must be run from the application-sdk repo root (so the upgrade tooling and docs are reachable).
Usage
/upgrade-v3 ../my-connector/src
/upgrade-v3 /absolute/path/to/connector/
Phase 0 — Setup and validation
- Parse
$ARGUMENTSto get the target path. If no argument is given, stop and ask the user for one. - Confirm the target path exists. If it does not, stop and report the error.
- Confirm you are running from within the application-sdk repo by checking that
tools/migrate_v3/rewrite_imports.pyexists. If it does not, stop and tell the user to run this skill from the application-sdk repo root. - Check the connector's SDK dependency. Read the connector's
pyproject.tomland look foratlan-application-sdk. The minimum supported v3 version for upgrades is 3.3.0 — earlier 3.x releases are missingCredentialRef.resolve(),self.upload()-via-FileReferenceplumbing, and the typed credential routing the rest of this skill assumes. The dependency must be:atlan-application-sdk>=3.3.0,<4.0.0- If it points to a v2 release (e.g.
atlan-application-sdk>=2.x) or an early v3 (>=3.0.0/>=3.1.0/>=3.2.0), bump the pin to>=3.3.0,<4.0.0and runuv sync. Refreshuv.lockin the same commit. - If the
pyproject.tomlstill contains a[tool.uv.sources]git override pointing atmainorrefactor-v3(a pattern used during v3 development), remove it — it's no longer needed now that v3.3.0 is on PyPI, and leaving it in pins the connector to an unstable ref. - If it already depends on
atlan-application-sdk>=3.3.0from PyPI with no git override, proceed.
- If it points to a v2 release (e.g.
4b. Check temporalio version. The v3 SDK requires temporalio with VersioningBehavior. Run in the connector repo root (where pyproject.toml is):
cd <connector-repo-root> && uv run python -c "from temporalio.common import VersioningBehavior"
If this fails with ImportError, upgrade temporalio before continuing:
cd <connector-repo-root> && uv add temporalio --upgrade && uv sync
- Read
tools/migrate_v3/MIGRATION_PROMPT.mdin full. This is the authoritative reference for all structural changes you will make. Do not proceed to Phase 3 without having read it. 5b. Read the upgrade guide atdocs/upgrade-guide-v3.mdin the application-sdk repo (also available at https://github.com/atlanhq/application-sdk/blob/main/docs/upgrade-guide-v3.md). This is the user-facing guide with v2 → v3 code examples for every upgrade step (imports, templates, handler, entry point, infrastructure, credentials, tests). Use it as a reference when making structural changes — it has the canonical before/after code snippets. - Run an initial checker pass to establish the baseline — do not fix anything yet:
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>
Print a short summary: how many FAILs and WARNs were found. If zero FAILs, tell the user the connector may already be migrated and stop.
Phase 1 — Mechanical import rewrites
Run the import rewriter across the entire target directory tree, including test files. This step is purely mechanical — import paths are rewritten losslessly; no logic is touched.
uv run python -m tools.migrate_v3.rewrite_imports <target-path>
Log every file that was changed. After the rewriter completes, tell the user which files were rewritten and how many import rewrites were applied.
Test files are NOT exempt from this phase. Deprecated import paths in tests must be updated just like production code — they are purely mechanical path changes. The constraint that applies to test files is that you must NEVER modify test logic, assertions, fixtures, or test data in any phase.
Phase 1b — Automated structural codemods
Before any AI-assisted structural work, run the codemod pipeline. This eliminates the mechanical transforms (decorator removal, signature rewrites, activity call rewrites, activities plumbing cleanup, entry point rewrite) deterministically, so the AI only needs to handle what remains.
uv run python -m tools.migrate_v3.run_codemods <target-path>
Review the output: files changed, any SKIPPED entries (dynamic dispatch or complex entry points that need manual attention), and errors.
Then re-run the checker to see what FAILs remain — those are the AI's work scope for Phase 2b:
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>
Log a short summary of what the codemods changed and what FAILs remain.
Extract structured context for Phase 2b — before writing any AI-assisted structural changes, run the context extractor to get a compact summary of what needs to be done. Use this summary when prompting yourself for Phase 2b instead of re-reading raw source files:
uv run python -m tools.migrate_v3.extract_context <target-path>
The output lists: connector type and confidence, difficulty estimate, classes with their roles and method inventories, infrastructure usage patterns, and any warnings about complex entry points or dynamic dispatch. Include this summary at the start of your Phase 2b analysis.
Phase 2 — Structural migration
Read the checker output from Phase 0 and the structure of the connector code to determine what structural work is needed.
2a — Identify connector type
Examine the source files in the target path (exclude test files from this analysis):
- Look for classes inheriting from
BaseSQLMetadataExtractionWorkflow/BaseSQLMetadataExtractionActivities→ SQL metadata extractor (§2a of MIGRATION_PROMPT.md) - Look for classes inheriting from
SQLQueryExtractionWorkflow/SQLQueryExtractionActivities→ SQL query extractor (§2b) - Look for classes inheriting from
IncrementalSQLMetadataExtractionWorkflow→ Incremental SQL extractor (§2c) - Look for HTTP/REST client usage (
httpx,aiohttp,requests, or customBaseClientsubclasses) with no SQL queries → REST/HTTP metadata extractor (§2d) - Look for any other
WorkflowInterface/ActivitiesInterfacesubclasses that don't fit above → Custom App (§3) - In all cases: identify the handler class (§4) and the entry point (§5)
- Count the total number of distinct
WorkflowInterfacesubclasses (exclude base classes from the SDK). If there is more than one, this is a multi-workflow connector — flag it and handle it in Phase 2a′ below before proceeding.
Tip — auto-detect the connector type:
uv run python -m tools.migrate_v3.check_migration --classify <target-path>This runs the F3 fingerprinter before the check pass and prints the detected connector type with confidence score and evidence.
2a′ — Choose transformation approach
After identifying the connector type, determine the transformation strategy:
SQL connectors (SqlMetadataExtractor, SqlQueryExtractor, IncrementalSqlMetadataExtractor):
- Default to the asset-mapper +
FileReference+self.upload()pipeline. This is the v3-native shape that lets each@taskstream rows into a typed file output and hand the path downstream — same asatlan-openapi-app. The fetch tasks returnFileReference-typed outputs, the publish step callsself.upload(UploadInput(local_path=..., storage_path=...)), and there is no sharedoutput_path/ scan-the-directory upload step. Inform the user:"v3 SQL connectors use the same asset-mapper + FileReference pipeline as REST connectors: each fetch task writes a typed file and returns a
FileReference;self.upload()carries it to object store. This replaces the v2output_pathdirectory +upload_to_atlan()scan pattern. Shall I proceed with this approach?" - Only fall back to the legacy transformer (
QueryBasedTransformer/AtlasTransformerinsidetransform_data()) if the user explicitly asks to minimize migration risk and accepts the cleanup follow-up. Note this in the manual-follow-up list — it preserves YAML query files and Daft DataFrames that the team is removing. - Do NOT keep the v2 "build one shared
output_path, write all fetch outputs there, scan-and-upload at the end" pattern. Each task returns its ownFileReference; uploads happen viaself.upload(). This was a top review finding on the MSSQL v3 PR.
Multi-workflow connectors (more than one WorkflowInterface subclass detected):
- Consolidate into a single
Appsubclass with one@entrypoint-decorated method per v2 workflow. All entry points share@taskmethods, the handler, andAppContext. ATLAN_APP_MODULEstays a singlemodule:ClassName— no comma-separated list.- See §5b of MIGRATION_PROMPT.md for the full pattern and
tests/integration/test_multi_entrypoint.pyfor a canonical example. - Inform the user: "This connector has N workflows. In v3 they become N
@entrypointmethods on one App class, sharing task helpers and the handler. I'll consolidate them into a single App."
REST/API connectors (BaseMetadataExtractor, Custom App):
- Default to the asset-mapper approach. This is the v3-native pattern (see
atlan-openapi-appas the reference implementation). Inform the user:"REST/API connectors in v3 use the asset-mapper pattern: typed Python records → pure Python mapper functions → pyatlan Asset instances → JSONL. This replaces the v2
QueryBasedTransformer/AtlasTransformerapproach. The reference implementation isatlan-openapi-app. Shall I proceed with this approach?" - If the user prefers to keep the existing transformer, respect that — but note it as a manual follow-up item in the summary.
Asset-mapper pattern summary (for reference when implementing):
Extract phase: API response → typed records (dataclass/msgspec.Struct) → JSONL files
Pass between tasks via FileReference
Transform phase: Read typed records from JSONL → mapper functions → pyatlan Asset instances
Write via asset.to_nested_bytes() → JSONL output file
Key elements:
app/asset_mapper.py— pure functions:map_<entity>(record, connection_qn, ...) -> pyatlan.Assetapp/api_types.py— typed intermediate records (dataclass ormsgspec.Struct)- No
TransformerInterface, no Daft DataFrames, no YAML query files - Uses
msgspec.jsonorjsonfor JSONL serialization FileReferencein task contracts to pass file paths between extract → transform tasks
2b — Apply structural changes
Incremental validation: Run
check_migrationafter each sub-step below. The checker output shows specific remaining FAILs — use it to guide the next step.
Follow the exact checklists in tools/migrate_v3/MIGRATION_PROMPT.md for the connector type(s) identified above.
Hard constraint — tests are completely out of bounds for structural changes:
- You MUST NOT modify test method bodies, assertions, fixtures, mock setup, or test data in any file under any directory whose name contains
testor starts withtest_. - You MUST NOT add, remove, or rewrite test cases.
- You MUST NOT change the logic of any existing test.
- The only change permitted in test files is the mechanical import rewrite already performed in Phase 1. If a test file needs structural changes to compile (e.g. it directly instantiates a v2 class that no longer exists), add a
# TODO(upgrade-v3): update test to use v3 APIcomment and leave the test body unchanged. The user will update tests manually after verifying the migration is correct.
Hard constraint — handler method signatures:
- Handler methods (
test_auth,preflight_check,fetch_metadata) MUST use typed contract parameters. Do NOT use*argsor**kwargs. - Correct:
async def test_auth(self, input: AuthInput) -> AuthOutput: - Forbidden:
async def test_auth(self, *args, **kwargs): - The checker will FAIL if
*args/**kwargsappear in a Handler subclass method.
Constraint — allow_unbounded_fields=True must not appear in connector contracts:
- The
connectionfield should useConnectionReffromapplication_sdk.contracts.types— the SDK provides this typed model for the well-known connection shape from AE/Heracles. metadata: dict[str, Any]is a contracting failure — the connector must know the shape of its inputs; type them explicitly.- Must NOT be used on inter-task Input/Output contracts — use
Annotated[list[T], MaxItems(N)]orFileReferenceinstead. - The checker will WARN on any
allow_unbounded_fields=True; reviewers will reject it.
Apply changes in this order:
-
App class — merge Workflow + Activities into the appropriate template subclass with
@taskmethods. Preserve all SQL query strings and business logic verbatim. For multi-workflow connectors, give each v2 workflow its own@entrypointmethod on the single shared App class (see §5b of MIGRATION_PROMPT.md); hoist duplicated activity helpers into shared@taskmethods.After completing this step, run:
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>Review any new/resolved FAILs (especially
no-v2-decorators,no-execute-activity-method) before proceeding. -
Handler — update base class, method signatures (typed contracts, no
**kwargs), removeload().fetch_metadatamust return the correct widget-specific output type:- SQL connectors →
SqlMetadataOutput(objects=[SqlMetadataObject(TABLE_CATALOG="...", TABLE_SCHEMA="...")]) - BI/API connectors →
ApiMetadataOutput(objects=[ApiMetadataObject(value="...", title="...", node_type="...", children=[...])]) - Do NOT use generic
MetadataOutputor deprecatedMetadataObject— these emitDeprecationWarningand will be removed in v3.1.0. - Import from
application_sdk.handler(e.g.from application_sdk.handler import SqlMetadataOutput, SqlMetadataObject).
After completing this step, run:
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>Review any new/resolved FAILs (especially
handler-typed-signatures) before proceeding. - SQL connectors →
-
Entry point — replace
BaseXxxApplicationinstantiation withrun_dev_combined()or CLI reference.After completing this step, run:
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>Review any new/resolved FAILs (especially
no-base-application) before proceeding. -
Infrastructure calls — replace
SecretStore/StateStore/ObjectStorecalls withself.context.*per §6 of MIGRATION_PROMPT.md.After completing this step, run:
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>Review any new/resolved FAILs (especially
no-dapr-client,use-app-state) before proceeding.
Work through one section at a time. After completing each section, check your changes are self-consistent before moving on.
2c — Directory consolidation
After completing the structural migration in 2b, consolidate the v2 directory layout. v2 connectors split logic across app/activities/ and app/workflows/; v3 uses a single flat file. For multi-workflow connectors this means all @entrypoint methods live in one file — do not split back into multiple files.
- Identify the main App class file (typically
app/activities/<name>.py, or whichever file holds the merged multi-workflow App). - Move it to
app/<app_name>.py(derive the filename from the App class or connector name, snake_cased). - If
app/workflows/<name>.pyexists and only re-exports from activities (e.g.from app.activities.<name> import MyConnector), delete it. - Delete the now-empty
app/activities/andapp/workflows/directories. - Update all production-code imports that referenced the old paths.
- For test-file imports pointing to the old paths:
a. Construct a JSON mapping of old module paths → new module paths from steps 1–4. For example, if
app/activities/metadata_extraction.pymoved toapp/metadata_extraction.py, the mapping is{"app.activities.metadata_extraction": "app.metadata_extraction"}. b. Run the internal import rewriter on the test directory:
c. For any symbol names that also changed (e.g.uv run python -m tools.migrate_v3.rewrite_imports \ --internal-map '{"app.activities.<name>": "app.<name>"}' \ <target-path>/tests/AnaplanMetadataExtractionActivities→AnaplanApp), manually update the import line's symbol name in each affected test file AND add a comment at the top of that file:# TODO(upgrade-v3): update references from OldClass to NewClass in test bodies. Do NOT modify test bodies, assertions, fixtures, or mocks. - Re-run the checker to confirm the
no-v2-directory-structureadvisory is gone.
If the connector does not have an activities/ or workflows/ directory, skip this step.
Phase 2d — Post-processing cleanup
After completing the structural changes in 2b and directory consolidation in 2c, run these cleanup steps to normalize imports and formatting before the validation loop:
# Remove unused imports and sort import order
uv run ruff check --fix --select I,F401 <target-path>
# Normalize formatting
uv run ruff format <target-path>
# Check migration status
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>
All FAIL checks should pass at this point. If any remain, address them before moving to Phase 3.
Positive-idiom checks (manual — the migration checker does not enforce these). Each one corresponds to a review finding that blocked the MSSQL v3 PR. Each must come up clean before Phase 3 — if any returns matches, refactor per the "v3 Task Idioms" section below.
# 1) No imports from private SDK modules (any segment starting with `_`).
# Catches both top-level (application_sdk._x) and nested (application_sdk.foo._y).
grep -rEn "from application_sdk[A-Za-z0-9_.]*\._[A-Za-z0-9_]" <target-path>/app/ \
&& echo "FAIL: private SDK import" || echo "OK: no private SDK imports"
# 2) No asyncio.to_thread inside @task code (use self.run_in_thread)
grep -rn "asyncio\.to_thread\b" <target-path>/app/ \
&& echo "FAIL: replace with self.run_in_thread" || echo "OK"
# 3) No os.environ reads in app/ — entry points (main.py / run_dev.py) live OUTSIDE
# the app/ dir or under a clearly-marked boundary. If your repo puts them under
# app/, exclude those paths explicitly.
grep -rEn "os\.environ\.get\b|os\.environ\[|os\.getenv\b" <target-path>/app/ \
&& echo "FAIL: move config to Input contract or AppConfig" || echo "OK"
# 4) No full-result materialization (cursor.fetchall / pd.read_sql_query)
grep -rEn "\.fetchall\b|pd\.read_sql_query\b|read_sql_query\b" <target-path>/app/ \
&& echo "FAIL: stream via fetchmany() instead" || echo "OK"
# 5) No raw context.get_secret(<guid>) for credentials — use CredentialRef.resolve(input).
# (Legitimate non-credential secret lookups are rare; if you have one, document it.)
grep -rn "context\.get_secret\b" <target-path>/app/ \
&& echo "FAIL: use CredentialRef.resolve(input) for credentials" || echo "OK"
# 6) No hand-rolled obstore / ParquetFileWriter / JsonFileWriter usage
grep -rEn "ParquetFileWriter\b|JsonFileWriter\b|obstore\." <target-path>/app/ \
&& echo "FAIL: use FileReference + self.upload()" || echo "OK"
# 7) allow_unbounded_fields should not appear — use ConnectionRef for connection fields,
# typed fields for metadata. Reviewers will reject dict[str, Any] escapes.
grep -rn "allow_unbounded_fields=True" <target-path>/app/ \
&& echo "WARN: avoid allow_unbounded_fields; use ConnectionRef / typed fields instead" || echo "OK"
These are guardrails, not blockers — a connector with a legitimate reason for any of these patterns may still ship, but it must be called out in the Phase 5 summary's manual-follow-up list with the reason. Default stance: refactor. Note: check #3 will hit any os.environ read inside app/; if the connector's entry point lives under app/main.py or app/run_dev.py, exclude those paths from the grep before treating a hit as a FAIL.
Phase 3 — Validation loop
Run the checker after completing Phase 2:
uv run python -m tools.migrate_v3.check_migration --no-color <target-path>
If FAILs remain:
- Read each failing item and the relevant source file.
- Fix the specific issue according to MIGRATION_PROMPT.md.
- Re-run the checker.
- Repeat until zero FAILs. Do not move to Phase 4 until the checker exits with code 0.
If only WARNs remain:
- Read each WARN item. If it is fixable without modifying test logic, fix it.
- If a WARN requires modifying test logic, skip it and add it to the manual follow-up list.
Phase 4 — Test run
Run the connector's test suite without modifying any test files:
cd <target-path> && uv run pytest --tb=short -q 2>&1 | head -80
If uv is not available in the connector repo, try python -m pytest --tb=short -q instead.
Do not modify any test to make it pass. If tests fail:
- Read the failing test and the code it exercises.
- If the failure is due to a production code issue introduced during upgrade (e.g. wrong method signature, missing attribute), fix the production code.
- If the failure requires understanding test intent or rewriting test logic, do NOT fix it. Add it to the manual follow-up list.
Phase 4b — E2E test generation
After the test suite run, check whether the connector has e2e tests using the v2 BaseTest / TestInterface pattern:
- Search for files under
tests/e2e/(ortests/integration/) that importBaseTestorTestInterface. - If found, read the original v2 e2e test file completely. List every test method and what it asserts before writing a single line of the new file.
- Generate a new equivalent e2e test file using the v3
application_sdk.testing.e2eAPI (§9 of MIGRATION_PROMPT.md):- For each test method in the original, generate a corresponding
async def test_xxx(deployed_app)function. The generated file MUST have at least as many test functions as the original has test methods. - Extract actual payload values from the original (hardcoded dicts,
default_payload()bodies, connection IDs) — do not substitute placeholder values like"test-connection"if the original has real values. - If an assertion checks response fields whose format changed (e.g.
result['authenticationCheck']), keep the assertion but add# TODO(upgrade-v3): response format changed — update field names. - Use the
AppConfigfixture with real values derived from the connector'spyproject.toml([project].namein PEP 621 / uv layout, or Helm chart values) — not generic placeholders.
- For each test method in the original, generate a corresponding
- Place the new file alongside the original, named
tests/e2e/test_<connector_name>_v3.py. - Add
# TODO(upgrade-v3): human must validate this test is equivalent to the originalat the top of the new file. - Do NOT delete or modify the original test file.
- Add the new test file to the manual follow-up list so the user knows to validate it.
If the connector has no v2-style e2e tests, skip this step.
Phase 4c — Contract generation
After tests pass and e2e tests are generated, the app needs a PKL contract that generates workflow config, credential config, runtime manifest, and typed Input class. Use the /make-contract skill (located in this repo at .claude/skills/make-contract/SKILL.md) to create or migrate the contract.
- Check if the connector already has a
contract/directory withapp.pkl:ls <target-path>/contract/app.pkl 2>/dev/null && echo "EXISTS" || echo "MISSING"
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 29
- Forks
- 17
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
upgrade-v3- Source
- github.com/atlanhq/application-sdk