make-contract
SkillDev toolsCreate, migrate, update, generate, or validate an Atlan native app Pkl contract with app-contract-toolkit and pkl. Use when helping an app author produce contract/app.pkl, contract/PklProject, app/generated artifacts, atlan.yaml decisions, and SDK input contracts — including migrating a legacy app onto the canonical App.pkl template, moving it off the archived toolkit repo, and raising it to the SDK floor that keeps its logs visible.
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 make-contract skill
What this skill tells your AI
The instructions your AI receives, as published by atlanhq/application-sdk in .agents/skills/make-contract/SKILL.md and read by ahel’s review.
Use this skill when the user asks to create, migrate, update, generate, or validate an Atlan native app contract. The goal is a correct Pkl contract and regenerated SDK artifacts — never hand-edited generated JSON.
This skill is adaptive and incremental. You inspect the app you are running in, classify what shape it is already in, propose a small plan, ask only blocking questions, then apply and verify one step at a time. You do not force a redesign on an app that does not need one.
Ground yourself in live sources first — do not trust memory or this file's snapshots
The toolkit moves. Earlier versions of this skill went stale because they hard-coded facts (which primitive to amend, the example list, a version-lookup recipe) that later changed. Derive volatile facts at runtime from the authoritative sources in this repo instead of trusting any list baked below:
- Primitive model, widgets, credential shapes, migration notes:
contract-toolkit/README.mdandcontract-toolkit/CLAUDE.mdare current. - Changelog / when something changed:
contract-toolkit/CHANGELOG.md. - The real example set:
git ls-files 'contract-toolkit/examples/*/app.pkl'(untrackedgenerated/and__pycache__leftover dirs are not examples). - Latest toolkit version (bare semver, from the SDK checkout root):
git tag --list 'contract-toolkit-v*' | sort -V | tail -1 | sed 's/^contract-toolkit-v//'. - The template source of truth:
contract-toolkit/src/App.pkl.
These contract-toolkit/..., git ls-files, and git tag lookups only resolve
from an atlanhq/application-sdk checkout — run them there (or git -C <sdk-repo>). From a consuming app repo they return nothing, silently.
contract-toolkit/AGENTS.md is stale (still describes the pre-consolidation
NativeApp.pkl world and references examples that no longer exist). Do not use
it as the source of truth; trust README.md + CLAUDE.md + CHANGELOG.md +
src/App.pkl.
Hard Rules
- Treat
atlanhq/application-sdkas the canonical source for this skill. The repo-local copies live at.agents/skills/make-contract/SKILL.mdand.claude/skills/make-contract/SKILL.md; mirror any edit between them byte-for-byte, and do not add nestedmake-contractcopies undercontract-toolkit/or app repos. App.pklis the canonical template.NativeApp.pklandNativeAppBundle.pklare frozen legacy. Author every new contract by amendingApp.pkl. Only touch the legacy modules when reading an app that still uses them (to migrate it, or to preserve it when the user declines).- Author, validate, and generate with
pklfrom the app repo root — this repo guarantees the pkl toolchain, not any external CLI. See Contract Commands. - Do not hand-edit generated files under
app/generated/except to inspect or compare. Fixcontract/app.pkl, then regenerate. - Keep examples public and generic. Prefer the tracked examples under
contract-toolkit/examples/; test-only scenarios live undercontract-toolkit/tests/fixtures/. - Preserve existing app behavior unless the user explicitly asks for a redesign.
- Never put secrets, real tokens, passwords, or tenant-specific data in contracts, examples, generated artifacts, or notes.
Adaptive Workflow
Work in this loop. Keep the main thread short; state a plan before you edit.
- Detect the app you are in (Discovery below).
- Classify its rollout bucket (Classification below) — fresh, legacy primitive, archived toolkit source, or below the version floor. An app can be in several buckets at once.
- State the plan in plain terms: app type, primitive, single- vs
multi-entrypoint, credential form, DAG/publish, generated path,
atlan.yamlownership, and which migrations (if any) apply. Name the version floor and whether the app clears it. - Ask only blocking questions — usually no more than three at a time, with concrete choices and a recommended default. Ask only where local repo evidence cannot settle a behavior-preserving choice.
- Apply one step at a time in
contract/app.pkl/contract/PklProject, then regenerate. - Verify the value flow and the
app_nameinvariant (below). - Finish with changed source files, generated artifacts, validation commands run, and any assumption or skipped check.
When the user is unsure, choose the simplest behavior-preserving contract:
single entrypoint, contract/app.pkl amending App.pkl, generated output in
app/generated, root atlan.yaml as packaging source of truth, and default
extract -> publish only for asset-producing connectors.
App Repo Layout
A contract-ized app has this shape. Everything at the root and under
app/generated/ is generated from contract/ — never hand-edit it; edit the
contract and regenerate.
your-app/
|-- atlan.yaml # generated — packaging source of truth; DO NOT EDIT
|-- app.yaml # generated — DO NOT EDIT
|-- .gitignore # must ignore app/generated/frontend/static
|-- main.py
|-- contract/ # the source you edit
| |-- app.pkl # single source of truth (amends App.pkl)
| |-- PklProject # toolkit package dependency
| `-- PklProject.deps.json # resolved package lock (from `pkl project resolve`)
`-- app/
`-- generated/ # served at ATLAN_CONTRACT_GENERATED_DIR
|-- __init__.py
|-- _input.py # pydantic AppInputContract
|-- {name}.json # workflow config (filename = contract name)
|-- atlan-connectors-{name}.json
`-- manifest.json
- Source vs generated: you edit only
contract/.atlan.yaml,app.yaml, and everything underapp/generated/are outputs — regenerate, don't edit. - Multi-entrypoint / bundle: each entrypoint's artifacts render under
app/generated/{entrypoint-name}/; the rootatlan.yaml/app.yamlown the bundle. See thebundleandcard-splitexamples. - An app may ship extra
contract/*.pklfiles (e.g. a reusable credential contract amendingCredential.pkl) that generate their ownapp/generated/*.json, plus e2e codegen files (_e2e_*.py) for agent-mode apps.app/generated/frontend/static/(playground dist) is gitignored.
contract-toolkit/README.md (§ App Repo Structure) is the live source if this
tree drifts.
Contract Commands (pkl-native)
This repo guarantees the pkl toolchain — there is no dependency on an external
CLI. Run every command from the app repo root (not from inside contract/).
Prereq: pkl >= 0.25.1 (brew install pkl).
# Resolve / refresh the dependency lock (writes contract/PklProject.deps.json):
( cd contract && pkl project resolve )
# Validate AND generate — the same command. A clean eval IS the validation
# (pkl type-checks the whole contract graph and fires every constraint), and it
# writes atlan.yaml, app.yaml, and app/generated/*.
pkl eval -m . contract/app.pkl
-m .sets the module output root to the app root so each output file lands at its natural path; running from insidecontract/or with a different-mwrites artifacts to the wrong tree.- There is no separate validate-only verb —
pkl evalis both validate and generate. - Scaffold: there is no init command. To start a new contract, copy
contract-toolkit/examples/minimal/app.pkltocontract/app.pkl, change its amend line toamends "@app-contract-toolkit/App.pkl", and create acontract/PklProjectwith the package dependency (see Migrations -> Archived toolkit URL for theuri). Then resolve and generate. The tracked examples amend a relative../../src/App.pkland ship noPklProject; a real app repo uses the package amend plus its owncontract/PklProject. - Update the toolkit version: edit the
["app-contract-toolkit"].uriversion incontract/PklProject, then re-run resolve + generate.
Capture the commands in the app's pyproject.toml so they are repeatable and
don't live only in the author's head. Add (or verify) the toolkit's recommended
poe task, and ensure poethepoet is a dev dependency:
[tool.poe.tasks]
generate-contract.shell = """
cd contract && pkl project resolve && cd ..
pkl eval -m . contract/app.pkl
"""
Then uv run poe generate-contract resolves and regenerates in one step. This is
the canonical wrapper (contract-toolkit/README.md § Recommended poe task) —
prefer it over any older per-app variant that evals into a subfolder and copies
the output up.
Discovery
Before editing, inspect the app shape:
git status --short --branch
find . -maxdepth 3 \( -name app.pkl -o -name PklProject -o -name atlan.yaml \)
# Which primitive does the contract amend?
grep -rn "amends" contract/*.pkl 2>/dev/null
# Toolkit dependency + URL
grep -rn "app-contract-toolkit" contract/PklProject contract/PklProject.deps.json 2>/dev/null
# SDK version the app pins
grep -REh "atlan-application-sdk" pyproject.toml uv.lock requirements*.txt 2>/dev/null | head
# Runtime wiring
rg -n "ATLAN_CONTRACT_GENERATED_DIR|app/generated|AppInputContract|@entrypoint|@task|workflowType|manifest.json"
Also answer:
- New contract, or migration from an existing one?
- Single-entrypoint or multi-entrypoint/bundle?
- Which generated directory does the app actually serve?
- Is root
atlan.yamlpresent and intentionally maintained? - Does it need credential config, preflight, publish controls, connection reference, multiple auth modes, or custom runtime steps?
Classification (rollout buckets)
Route the app by what Discovery found:
- Fresh — no contract yet, or already amends
App.pklon a current toolkit and clears the version floor. Author/edit directly; no migration. - Legacy primitive —
contract/app.pklamendsNativeApp.pklorNativeAppBundle.pkl. Propose migration toApp.pkl(see Migrations). - Archived toolkit source —
PklProjectpoints at the old standalone toolkit URL (package://atlanhq.github.io/app-contract-toolkit/...) instead of the SDK-hosted one. Propose switching the URL and bumping the version. - Below the version floor — SDK
< 3.21.0or toolkit< 0.17.0. Propose raising both (this is what keeps logs visible — see theapp_nameinvariant).
Migrations are opt-in and incremental. Detect, propose, get a yes, then apply one at a time — never silently redesign.
Version Floor (why it exists)
Enforce SDK >= 3.21.0 and contract-toolkit >= 0.17.0. Below this, an
app's failure logs can silently fail to surface in the Workflow Center. Hard-fail
generation/validation guidance if the app is below the floor and the user has
not explicitly opted to stay.
Why 3.21.0 / 0.17.0 (not 3.18):
app_nameis the app's log-correlation identity. Logs are written tagged withapp_name=ATLAN_APPLICATION_NAME(fromatlan.yaml). The UI reads logs back by filtering on theapp_namevalue baked into the served manifest.- Pre-fix, the manifest shipped a literal
"{app_name}"placeholder that the SDK substituted at serve time from the runtime class name (GlueApp->glue-app), not the contractname(glue). Write-identity != read-identity -> the log query matches zero rows -> the panel shows "no logs" though logging worked. This is the failure signature the fix addresses. - The fix bakes
app_namefrom the contractnameat generate time and removes the runtime placeholder, so read-identity equals write-identity by construction:- SDK
3.19.0/ toolkit0.14.2— core bake forApp.pklapps. - SDK
3.20.2— output-path derivation prefers the resolved app name. - SDK
3.21.0/ toolkit0.17.0— same bake applied to the legacyNativeApp.pklcrossover template.
- SDK
3.21.0/0.17.0is the single floor that covers both theApp.pklandNativeApp.pklfamilies.3.18.0carries none of these fixes.
The app_name invariant (same name everywhere)
The contract name (kebab-case) is the single root identity. The toolkit
derives everything from it, and every surface must stay byte-identical:
| Surface | Must equal |
|---|---|
contract name in contract/app.pkl | the root value |
generated {name}.json and atlan-connectors-{name}.json filenames | name |
manifest app_name (top-level and inputs.app_name) | name |
task-queue prefix atlan-{name}-... | name |
atlan.yaml app name -> ATLAN_APPLICATION_NAME -> the log tag | name |
runtime app identity (_app_name / resolved app name) | name |
If atlan.yaml name diverges from the contract name, the log write-tag and
the manifest read-filter disagree and logs stop surfacing. Fix the divergent
surface to match the intended kebab-case identity — correct contract/app.pkl
name or atlan.yaml, never hand-edit app/generated/.
Verify after every generate:
NAME=$(grep -E '^\s*name\s*=' contract/app.pkl | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
echo "contract name = $NAME"
# No literal placeholder may survive in generated manifests (smoking gun):
grep -rn '"{app_name}"' app/generated/ && echo "FAIL: unbaked placeholder — regenerate on toolkit >= 0.17.0"
# Every baked app_name must equal $NAME:
grep -rn '"app_name"' app/generated/ 2>/dev/null
# atlan.yaml packaging name must equal $NAME:
grep -rn "name" atlan.yaml app/generated/atlan.yaml 2>/dev/null
# Generated filenames must be {name}.json / atlan-connectors-{name}.json:
ls app/generated | grep -E "^(${NAME}|atlan-connectors-${NAME})\.json$"
Migrations
Each migration is small and mechanical. Propose, confirm, apply, regenerate, verify.
Archived toolkit URL -> SDK-hosted package
The toolkit was once a standalone repo. It is now hosted from the SDK repo. If
contract/PklProject points at the old standalone URL, switch it and pin the
latest version:
dependencies {
["app-contract-toolkit"] {
uri = "package://atlanhq.github.io/application-sdk/contracts/app-contract-toolkit@<LATEST>"
}
}
Then re-resolve and regenerate (see Contract Commands) — pkl project resolve
rewrites PklProject.deps.json. Get the bare-semver <LATEST> (from the SDK
checkout) with
git tag --list 'contract-toolkit-v*' | sort -V | tail -1 | sed 's/^contract-toolkit-v//'.
NativeApp.pkl / NativeAppBundle.pkl -> App.pkl
- Change the amend line to
amends "@app-contract-toolkit/App.pkl". - Credential field primitives (
FieldSpec,AuthOption,ConditionalFieldSpec,NamedWidget,NamedProperty) are now defined inApp.pkl, and theWidgets.CredentialInputwidget is re-exported by it — all reachable by name, no separate import. (CredentialInputis auiConfigwidget, not a credential field entry — don't use it insidecredentialAuthOptions.) - Widgets come from
Widgets.*(re-exported byApp.pkl);Connectors.pklstill needs an explicitimport. - Single-entrypoint: leave
entrypointsempty; setuiConfig+pipeline.App.pklsynthesises the implicit entrypoint. - Multi-entrypoint / bundle: replace the
NativeAppBundle.pklroot plus NNativeApp.pklper-entrypoint contracts with oneApp.pklroot that populatesentrypoints, each pointing at a per-entrypoint contract that also amendsApp.pkl.
Confirm the exact current field names against contract-toolkit/README.md and
src/App.pkl before writing — do not trust this summary over the live source.
Raise to the version floor
Bump the SDK dependency to >= 3.21.0 and the toolkit to >= 0.17.0, then
regenerate so the manifest bakes a literal app_name. Re-run the app_name
invariant checks.
Primitive Model (App.pkl)
- One
amends "@app-contract-toolkit/App.pkl"line is all any contract needs. - Single- vs multi-entrypoint is
entrypointsempty vs populated — not two different modules. - Marketplace-card presence: single-entrypoint uses
marketplaceCard: Boolean; multi-entrypoint controls each card via the entrypoint'spackageId(set it to make the entrypoint a card; omit to keep it routable-only). emitEntrypointsis deprecated and slated for removal — do not adopt it in new work; use per-entrypointpackageIdinstead. (An existing app that sets it is a pattern to migrate off, not to copy.)- Keep
flatManifestArgs = trueunless an existing app intentionally reads metadata-wrapped args; changing it changes the runtime payload shape — call it out.
Prefer typed toolkit APIs over raw JSON: typed widgets from Widgets.*, typed
credential primitives from App.pkl, Connectors.pkl constants, SQL/JDBC
helpers (JDBCUrlAuthOption, AdvancedJDBCUrlGroup, SqlTree), and runtime
graph nodes (PublishNode, QueryIntelligenceNode, PopularityNode,
LineageNode, LineagePublishNode), dropping to raw DAGNode only when a typed
node does not fit.
FileReference and App.upload(): If your contract carries a
FileReferencethat must reach a downstream Atlan system app (publish, lineage, quality), the connector must callApp.upload()explicitly fromrun()— the task-to-task activity interceptor only writes to the customer-ownedobjectstore. See ADR-0014.
Value-Flow Rules
Before finishing, trace every user-provided value through the generated artifacts:
workflow field -> widget type -> _input.py field/type -> manifest placeholder -> runtime arg
credential field -> credential config -> workflow credential reference -> runtime credential lookup
DAG node id -> dependency condition -> manifest node -> runtime workflow/activity
- Prefer typed properties over arbitrary string keys; use a node's typed
property instead of
extraArgsor rawDAGNode.args. - Do not expose a runtime default as a setup field just to pass the same value through; omit it and let the runtime own its default.
- Placeholders must be exact workflow field names like
"{{include-filter}}"; no dotted placeholders unless the consuming runtime supports them. - Every manifest placeholder must have a matching workflow/credential field, or be intentionally supplied by the SDK/runtime and documented in the final note.
- Boolean and numeric controls use typed widgets and typed node properties — avoid stringly-typed contracts a user can typo into a runtime-only failure.
Examples (derive the current set at runtime)
The tracked examples all amend App.pkl and are the public reference. List
them live — do not trust a baked list:
git ls-files 'contract-toolkit/examples/*/app.pkl' | sed 's#.*/examples/##;s#/app.pkl##'
At the time of writing the tracked set is: minimal (smallest contract, start
here), full (every overridable feature), bundle (multi-entrypoint), card-split
(multi-entrypoint where only one entrypoint is a card), connection-ref
(ConnectionRefInput), publish-controls (publish toggles), fanin (fan-in via
dependsOn), deploy (single-pool KEDA/resources), pools (named worker
pools), scheduled (cron background job), behind-the-scenes
(marketplaceCard = false), agent-e2e (agent-mode codegen). Re-run the command
above to catch additions or removals.
Node-specific coverage (popularity, query-intelligence placeholders) lives in
contract-toolkit/tests/fixtures/*.pkl, not in examples/.
Canonical Apps (public first-party references)
When you need a real, end-to-end reference, inspect these public connector apps. Note each one's caveat — they are pattern references, not version references (none is on the current floor yet):
atlanhq/atlan-mysql-app— canonical SQL/JDBC single-entrypoint with multi-mode auth (basic + IAM user + IAM role) and a full lineage-publish DAG. Caveat: still on legacyNativeApp.pkland the archived toolkit URL — it is itself a migration candidate, so read it for the SQL/DAG patterns, not the primitive or the dependency URL.atlanhq/atlan-metabase-app— best modernApp.pklreference: REST auth, a rich multi-node DAG, and the correct "two code@entrypoints, one marketplace card" shape via single-entrypoint contract mode (noentrypointslisting, so themarketplaceCarddefault applies). It has already migrated off the deprecatedemitEntrypoints = falseworkaround — copy this pattern, not that.atlanhq/atlan-openapi-app— URL-vs-cloud import modes plus an app-owned external object-store credential contract (aCredential.pklamend). Caveat: task-only (no DAG/publish) and on an older toolkit.
Validation
Validate and generate with pkl from the app root (see Contract Commands) — a
clean pkl eval is the validation and writes the artifacts in one step:
( cd contract && pkl project resolve )
pkl eval -m . contract/app.pkl
Then verify generated output and invariants:
ls app/generated
python -m py_compile app/generated/_input.py
find app/generated -name '*.json' -print0 | xargs -0 -I{} python -c 'import json,sys; json.load(open(sys.argv[1])); print("OK", sys.argv[1])' {}
- Generated JSON filenames match the contract
name. - No
"{app_name}"literal survives inapp/generated/, and every bakedapp_nameplus theatlan.yamlname equals the contractname. manifest.jsonplaceholders match fields emitted by workflow and credential config.- Credential config name matches the credential input type the workflow uses.
- SQL tree credential type and field names line up.
- Optional publish controls match the app's expected output behavior.
- Root
atlan.yamland generated artifacts agree on entrypoint names. app/generated/_input.pyimports cleanly in the app's active environment.
For toolkit changes under contract-toolkit/, use the validation commands in
contract-toolkit/CLAUDE.md and keep examples, tests, and docs in sync.
Finish
Summarize:
- What contract source changed and which migrations were applied.
- Which generated artifacts changed.
- Which
pklcommands ran (resolve / eval), and theapp_nameinvariant result. - Whether the
generate-contractpoe task was added or verified in the app'spyproject.toml. - The SDK/toolkit versions before and after, relative to the floor.
- Any skipped validation and the concrete reason.
Signals
- GitHub stars
- 29
- Forks
- 17
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
make-contract- Source
- github.com/atlanhq/application-sdk