author-composition
Use this skill when the user asks to create, extend, modify, or debug a Crossplane composition function in a control-plane project — in any language (KCL, Python, TypeScript, Go). Detects the function
SKILL.mdskills/author-composition/SKILL.md
GitHubComposition Authoring
Author and modify Crossplane composition functions, in any language up function generate
supports, test first.
Before you start
A composition's meaning is language-agnostic; only its syntax differs. Read three layers:
the charter (the rules), patterns.md (what each pattern means), and
your language's file under control-plane-project-charter/references/languages/ (the
syntax).
- Reviewing function code: the checks are in
control-plane-project-charter/references/charter/review.md. A test passes but the resource misbehaves on a control plane: patterns.md. - Tests →
author-tests; XRD design and scaffolding →author-configuration-package; the gate and deploying →verify-configuration; live cloud runs →e2e-test-configuration; migrating a function to v2 →plan-v2-migration(the function-side points: patterns.md).
Binding rules — they hold even if you open nothing else
Load control-plane-project-charter first, or read its SKILL.md beside this skill's
directory: this skill does not load it, and the charter has the reasons. The project's own API,
design or gate script wins over these: say where you departed.
- Never block on a question nobody can answer. Interactive, ask only what the project can't tell you; unattended, never ask: decide from the spec and state the assumption, or stop and report (charter §1).
- Never create a group, Space, control plane or cloud resource as a side effect, and never
pass
--publicyourself (§9). Runup test run --e2eonly after loading e2e-test-configuration: it has each target's preconditions, and the read-back and teardown rules. - Test first: watch each new composition or unit test fail for the reason you intended;
for a new E2ETest this is optional. A syntax error, a missing path,
✗ Parsing tests, a compile error or a bug in the test's own logic is a broken test, not RED. Backfill by mutating the implementation, never the expected value (§3). - In a v2 project, managed resources carry
forProvideronly, on the.m.API groups, unless the project's spec or API sets more: nodeletionPolicy,managementPoliciesormetadata.namespace; omitproviderConfigRefif and only ifClusterProviderConfig/defaultexists and is the right one. One whose connection details the function reads also needswriteConnectionSecretToRef, and a missing detail never falls back to a value. A v1 project stays v1 (§5). - Claim only what ran: the command's own exit code, not
tail's: redirect, then read$?(cmd > /tmp/x.log 2>&1; echo "exit=$?"); after a pipe, bash${PIPESTATUS[0]}, zsh$pipestatus[1].No test files foundmeans nothing ran. Name the layer you reached — render, composition test, local control plane, cloud — and never claim one you did not reach; for each new test, the change that turns it red, or call it unproven. Comments and docs claim no more (§4, §8).
Phase 1: Detect the language and the Crossplane generation — do not ask
Language: detect it as charter §10 says and read the language file before writing a line;
its layout, imports and bootstrap are wrong by default if you guess. No functions yet: take it
from upbound.yaml, else an existing function, else ask — a decision, not a discoverable fact.
Generation, which governs more of the guidance than the language does:
apis/*/definition.yaml: apiextensions.crossplane.io/v2 → v2: binding rule 4 and charter §5 apply
/v1 → v1: they do not
A v1 project uses the non-.m. models and a cluster-scoped ProviderConfig, and its shipped
function breaks every v2 rule. Read the generation from the XRD, never from the template a
project started as: the templates differ
(control-plane-project-charter/references/charter/generators.md). Match what the project is; migrating it is plan-v2-migration's work, not cleanup in passing.
Phase 2: Discover — do not ask
Make sure the XRD and a composition exist: the test points at them. Write the XRD yourself
(author-configuration-package;
control-plane-project-charter/references/charter/v2-resources.md has the v2 skeleton).
Scaffold the composition with up composition generate, which emits only an auto-ready step,
and wire your function in with up function generate <n> <composition-path>. Leave the
function body empty or unchanged.
control-plane-project-charter §2 has the general table. The composition-specific additions:
| What you need | How to get it — no question required |
|---|---|
| Models missing (fresh clone, or only your XR's model missing) | .up/ is gitignored. Run up project build. It resolves and caches the dependencies itself and writes every model, your XR's included (observed with up v0.55.0, with an empty --cache-dir and no .up/). up dep update-cache refreshes only the dependency models |
| Function layout, import line, class names, field names and types | the generated models under .up/<language>/, read as the language file says (Go: control-plane-project-charter/references/languages/go.md, "Imports and models") |
Python: before Phase 3, run what
control-plane-project-charter/references/languages/python.md says with this skill's
scripts/: scripts/setup_venv.py, scripts/probe_project.py (layout, imports,
fields, generation) and scripts/run_function.py (the fast tier).
Never hand-derive an import path and never guess a provider field name. Both are one command away.
Worth asking, and only if args does not already say: optional/conditional resources, whether
connection details must reach the XR, and whether child XRs are composed. If unstated, choose
the simplest correct behaviour, say so in your summary, and continue.
Phase 3: Design against what you discovered
patterns.md has each design question in full. In brief:
- Layout — match what the project already uses; never convert one to the other.
- Composition keys — they become
crossplane.io/composition-resource-name, and Crossplane derives resource names from them. A rename is a migration. - References before status plumbing. If resource B needs an ARN or ID from resource A,
a
*Reffield makes the provider resolve it, so the composition stays single-pass and never reads A's status. - List fields. One element → one resource is a decision you justify, not a default (charter §6).
- Conditional resources — which use
ready OR exists, and where each belongs relative to the existing guard clauses (patterns.md: the guard-clause chain). - Flexible maps — tags and labels need
additionalPropertiesin the XRD; fixedproperties:produce null-value failures in typed languages.
Phase 4: RED — write the failing test before the implementation
Binding rule 3, before any function code; charter §3 owns the loop. Write what the design fixes — keys, Kinds, fields — as an assertion now, while it states intent.
Who does what in a run that changes the function. author-tests writes the test (its
Phases 1–4, Phase 2 for what no render reaches). This skill runs RED (below), GREEN
(Phase 5) and REFACTOR (Phase 6). The gate comes once, after this skill's Phase 6, as
author-tests Phase 6 describes. A run that only adds or changes tests stays in author-tests
throughout.
Run up test run "tests/<t>" and keep the failure text; the reason must be one charter §3's
table counts as RED. Adding to a composition that already works: confirm the new assertion
fails while the others pass.
Phase 5: GREEN — implement until the test passes
Implement until the test passes, and no further. Re-run up test run "tests/<t>" and report
the RED→GREEN transition, not only the final green.
The language file has the bootstrap and the syntax. Language-independent, in order:
- Parse the observed XR with the language's required bootstrap (Python needs
struct_to_dict; skipping it fails silently on current Up CLI versions. Go: the generated models are notruntime.Objects, so convert through JSON, in and out). - Create managed resources per binding rule 4.
- Convert flexible maps to the language's plain map type before assigning them.
- Extract connection details by composition key, not by resource name.
- Mark a composed ProviderConfig — anything auto-ready cannot judge — ready explicitly.
- Give each optional resource its own conditional block rather than another early return (patterns.md: the guard-clause chain).
For iterating on a crash rather than an assertion, use the fast tier where the language
offers one (Python: run_function.py; Go: go test ./... in functions/<n>/ — the scaffold's
fn_test.go is an empty table that passes with zero cases, so add one before you count it).
It supplements the loop and never replaces it.
Phase 6: REFACTOR and verify — coverage, not a green exit code
With the suite green, tidy the implementation, then add the next failing assertion and repeat. Before you call anything covered (charter §8):
- Check provider validity, not only v2 conformance, as charter §6 says; write the result in your summary.
- Grep your own function with the two greps in
control-plane-project-charter/references/charter/v2-resources.mdand judge each hit rather than counting them. Legitimate: anamespacechosen by a cluster-scoped XR or set on an object embedded insideforProvider(a provider-kubernetesObjectmanifest); aproviderConfigRefwhere the right config is notClusterProviderConfig/default, with akindnaming an object that exists; any field the project's spec or API sets. Anamespaceon a composed Secret or ConfigMap of a namespaced XR is not legitimate: Crossplane overwrites it. - Read the render, not the assertions:
up test run "tests/test-*" --function-logs, then read it ascontrol-plane-project-charter/references/charter/evidence.mdsays (the directory the run prints asTest artifacts written to <dir>). Add each emitted resource toassertResources; until you do, it is untested even though the suite is green. - The suite satisfies
control-plane-project-charter/references/charter/evidence.md"Coverage": each input shape including a minimal XR, each observed-state branch, everystatusfield on the composite.
Phase 7: Report
Claim only what ran (binding rule 5, charter §4). Composition tests render and assert; they talk to no provider and install nothing, so never "production-ready", "deployable" or "verified".
Out of reach locally: external-name semantics — whether a resource's external name is
provider-assigned or the identifier you set — are undiscoverable from CRDs or models, and
assertResources is partial-positive, so a stray annotation is never flagged. Check it on
a live control plane, or say it is unchecked.
Success criteria
Checks for you before you report, not a report format (control-plane-project-charter §4).
- The function language and generation were detected, not assumed, and the matching
languages/file was read - Import paths resolved from the project, never derived by hand
- Managed resources follow binding rule 4, with no dangling
providerConfigRef - A test was written first, run, and observed to fail for the right reason
- The RED→GREEN transition is reported, with the failure text
- The render was read and every emitted resource is asserted
- Provider-level constraints were checked, and the result — including "found nothing" — is in the summary
- The summary claims the layer that was actually reached, and no further