/upgrade-v3

SkillDev tools

Upgrade 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.

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

  1. Parse $ARGUMENTS to get the target path. If no argument is given, stop and ask the user for one.
  2. Confirm the target path exists. If it does not, stop and report the error.
  3. Confirm you are running from within the application-sdk repo by checking that tools/migrate_v3/rewrite_imports.py exists. If it does not, stop and tell the user to run this skill from the application-sdk repo root.
  4. Check the connector's SDK dependency. Read the connector's pyproject.toml and look for atlan-application-sdk. The minimum supported v3 version for upgrades is 3.3.0 — earlier 3.x releases are missing CredentialRef.resolve(), self.upload()-via-FileReference plumbing, 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.0 and run uv sync. Refresh uv.lock in the same commit.
    • If the pyproject.toml still contains a [tool.uv.sources] git override pointing at main or refactor-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.0 from PyPI with no git override, proceed.

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
  1. Read tools/migrate_v3/MIGRATION_PROMPT.md in 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 at docs/upgrade-guide-v3.md in 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.
  2. 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 custom BaseClient subclasses) with no SQL queries → REST/HTTP metadata extractor (§2d)
  • Look for any other WorkflowInterface / ActivitiesInterface subclasses 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 WorkflowInterface subclasses (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 @task stream rows into a typed file output and hand the path downstream — same as atlan-openapi-app. The fetch tasks return FileReference-typed outputs, the publish step calls self.upload(UploadInput(local_path=..., storage_path=...)), and there is no shared output_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 v2 output_path directory + upload_to_atlan() scan pattern. Shall I proceed with this approach?"

  • Only fall back to the legacy transformer (QueryBasedTransformer / AtlasTransformer inside transform_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 own FileReference; uploads happen via self.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 App subclass with one @entrypoint-decorated method per v2 workflow. All entry points share @task methods, the handler, and AppContext.
  • ATLAN_APP_MODULE stays a single module:ClassName — no comma-separated list.
  • See §5b of MIGRATION_PROMPT.md for the full pattern and tests/integration/test_multi_entrypoint.py for a canonical example.
  • Inform the user: "This connector has N workflows. In v3 they become N @entrypoint methods 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-app as 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/AtlasTransformer approach. The reference implementation is atlan-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.Asset
  • app/api_types.py — typed intermediate records (dataclass or msgspec.Struct)
  • No TransformerInterface, no Daft DataFrames, no YAML query files
  • Uses msgspec.json or json for JSONL serialization
  • FileReference in task contracts to pass file paths between extract → transform tasks

2b — Apply structural changes

Incremental validation: Run check_migration after 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 test or starts with test_.
  • 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 API comment 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 *args or **kwargs.
  • Correct: async def test_auth(self, input: AuthInput) -> AuthOutput:
  • Forbidden: async def test_auth(self, *args, **kwargs):
  • The checker will FAIL if *args/**kwargs appear in a Handler subclass method.

Constraint — allow_unbounded_fields=True must not appear in connector contracts:

  • The connection field should use ConnectionRef from application_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)] or FileReference instead.
  • The checker will WARN on any allow_unbounded_fields=True; reviewers will reject it.

Apply changes in this order:

  1. App class — merge Workflow + Activities into the appropriate template subclass with @task methods. Preserve all SQL query strings and business logic verbatim. For multi-workflow connectors, give each v2 workflow its own @entrypoint method on the single shared App class (see §5b of MIGRATION_PROMPT.md); hoist duplicated activity helpers into shared @task methods.

    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.

  2. Handler — update base class, method signatures (typed contracts, no **kwargs), remove load().

    fetch_metadata must return the correct widget-specific output type:

    • SQL connectorsSqlMetadataOutput(objects=[SqlMetadataObject(TABLE_CATALOG="...", TABLE_SCHEMA="...")])
    • BI/API connectorsApiMetadataOutput(objects=[ApiMetadataObject(value="...", title="...", node_type="...", children=[...])])
    • Do NOT use generic MetadataOutput or deprecated MetadataObject — these emit DeprecationWarning and 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.

  3. Entry point — replace BaseXxxApplication instantiation with run_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.

  4. Infrastructure calls — replace SecretStore/StateStore/ObjectStore calls with self.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.

  1. Identify the main App class file (typically app/activities/<name>.py, or whichever file holds the merged multi-workflow App).
  2. Move it to app/<app_name>.py (derive the filename from the App class or connector name, snake_cased).
  3. If app/workflows/<name>.py exists and only re-exports from activities (e.g. from app.activities.<name> import MyConnector), delete it.
  4. Delete the now-empty app/activities/ and app/workflows/ directories.
  5. Update all production-code imports that referenced the old paths.
  6. 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.py moved to app/metadata_extraction.py, the mapping is {"app.activities.metadata_extraction": "app.metadata_extraction"}. b. Run the internal import rewriter on the test directory:
    uv run python -m tools.migrate_v3.rewrite_imports \
      --internal-map '{"app.activities.<name>": "app.<name>"}' \
      <target-path>/tests/
    
    c. For any symbol names that also changed (e.g. AnaplanMetadataExtractionActivitiesAnaplanApp), 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.
  7. Re-run the checker to confirm the no-v2-directory-structure advisory 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:

  1. Search for files under tests/e2e/ (or tests/integration/) that import BaseTest or TestInterface.
  2. 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.
  3. Generate a new equivalent e2e test file using the v3 application_sdk.testing.e2e API (§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 AppConfig fixture with real values derived from the connector's pyproject.toml ([project].name in PEP 621 / uv layout, or Helm chart values) — not generic placeholders.
  4. Place the new file alongside the original, named tests/e2e/test_<connector_name>_v3.py.
  5. Add # TODO(upgrade-v3): human must validate this test is equivalent to the original at the top of the new file.
  6. Do NOT delete or modify the original test file.
  7. 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.

  1. Check if the connector already has a contract/ directory with app.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