Generate a backstage.yaml catalog file for a product or system following internal conventions. Use when the user asks to create, scaffold, or update a backstage.yaml, Backstage catalog, or service catalog entry.
Installation
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Generate a backstage.yaml catalog file for a product or system following internal conventions. Use when the user asks to create, scaffold, or update a backstage.yaml, Backstage catalog, or service catalog entry.
tools
Read, Glob, Grep, Write, AskUserQuestion
license
MIT
metadata
{"author":"eirikeikaas","version":"1.1.1"}
Backstage Catalog Generator
Generate a multi-document backstage.yaml for a product/system following internal catalog conventions.
The output should be readable by a developer who has never opened Backstage before. Prefer narrative descriptions over boilerplate, lean on links and annotations that make the page useful day-to-day, and use the relations that encode reality — not approximations.
Workflow
Step 1: Gather Context
Before generating anything, understand the system:
Check for an existing backstage.yaml in the repo root using Glob. If one exists, read it — you may be updating, not creating from scratch.
Inventory the codebase to infer components, tech stack, and structure:
Monorepo? Use Glob/ls on apps/*, packages/*, services/*. Enumerate every subdirectory explicitly before deciding what to model — don't just spot-check the obvious ones.
apps/* and services/* directories → each one is a candidate Component. Includes deployables (web, worker, mobile), CLI tools, build-time scripts, anything with its own package.json. If you skip one, leave a one-line YAML comment in the file explaining why (e.g. # parser/ is a one-shot CSV generator script, not modeled).
packages/* directories → internal workspace libraries. Do NOT model these as Components by default. They're implementation detail and clutter the catalog. Skip them unless: (a) they're explicitly consumed across multiple separate products (a shared TRY-wide library — in which case it should live in try-backstage-catalog, not the product file), or (b) the user explicitly asks. Do NOT add dependsOn: component:<workspace-lib> edges — workspace package deps stay invisible to the catalog.
DB/ORM config, migrations, queue config, KV/Durable Object bindings — these become Resources.
API route handlers, OpenAPI specs, websocket handlers — these become APIs (and providesApis on the host Component).
Outbound HTTP calls (env vars like *_URL, SDKs for Storyblok/Centra/etc.) — these become consumesApis.
Read source for the specifics that will appear in the file. Inventory tells you what entities exist; this step extracts the literal strings you'll write down. Do not infer these from memory of a similar product — verify each one against this repo.
For every local API entity: open the route file (e.g. apps/<app>/src/app/api/<path>/route.ts) with the Read tool and capture: the actual HTTP method exported (GET, POST, …), the actual env-var name used for auth (e.g. REVALIDATE_SECRET vs REVALIDATE_TOKEN), the handler library, and the source path. These go straight into the API entity's definition.
For every infra-config file (wrangler.jsonc, vercel.json/vercel.ts, Terraform, etc.): use the literal binding/queue/DO/KV names as the basis for Resource metadata.name. If you choose to use a tidier catalog name, put the real infra name in the Resource's description so devs can cross-reference dashboards.
For caller-side env vars (e.g. the worker's REVALIDATE_URL): confirm the URL points at the route you modeled — that's what closes the consumesApis ↔ providesApis loop.
⚠️ Anti-pattern: borrowing from sibling projects. TRY products share libraries and stack choices — same Storyblok+Centra+Cloudflare-revalidate-worker shape, same @frend-digital/cache/next. Strong pattern-similarity is exactly when you're most tempted to fill in env var names, HTTP methods, resource binding names, Centra admin paths, Storyblok space IDs, Cloudflare account IDs, queue names, and worker names from memory of another repo. Don't. Same library family ≠ same secrets, routes, slugs, or IDs — two products from the same agency can share the entire stack and still have entirely different values for every one of those fields. Every specific in the generated file must trace back to a file you read in this session, in this repo — or to an explicit answer from the user.
Reference shared services by the canonical naming convention — don't redefine third-party services locally. See Shared catalog references below for the patterns.
Ask the user (via AskUserQuestion) for anything you cannot infer:
System name, title, description
Owning GitHub team slug (not a friendly name — see Owner convention below)
Lifecycle (production, development, experimental)
SLA tier/agreement (if applicable)
Any external/partner APIs consumed that aren't already in the shared catalog
Centra admin path — if the product uses Centra, the admin URL is https://<store>.centra.com/<admin-path> and the path varies per client (ams2019, ams2020, etc.). Always ask; do not assume.
HubSpot company ID — the numeric ID for the client's HubSpot company record, used for the hubspot.com/company-id annotation. Not derivable from the repo; ask the user (they'll usually find it in HubSpot's URL when viewing the company).
Step 2: Generate the backstage.yaml
Write the file to the repository root as backstage.yaml.
Always start the file with a header comment block explaining the entity kinds and house conventions (see Header comment below). This is non-negotiable — it's the first thing every reader sees and is the single biggest onboarding lift.
Use the entity ordering, naming, and conventions described below.
No dependsOn + consumesApis pointing at the same target.
Owner references resolve to actual GitHub teams (or other groups in try-backstage-catalog/org/).
Every annotation that requires a real value has one (no <placeholder> left behind).
Header comment
Every generated file MUST start with a short header that teaches the model. Adapt the wording to the product, but keep all four rule lines:
# Backstage catalog manifest for the <Product> product.## Quick primer:# System = the product as a whole.# Component = something we build and deploy (web app, worker, mobile app).# Resource = infrastructure we depend on (queues, durable objects, log drains, hosting).# API = an HTTP/websocket surface — third-party or one of our own.## House conventions:# - consumesApis → who CALLS an API (HTTP direction). Use for third-party AND# our own component-to-component calls.# - providesApis → who EXPOSES an API. Put on the producing component, with a# matching API entity in this file (or in try-backstage-catalog).# - dependsOn → platforms we run on, or runtime/data bindings (queue, durable# object, log drain). NOT for HTTP — that's consumesApis.# - owner → always a GitHub team slug, written as group:default/<slug>.## Reference catalog: https://github.com/tryhuset/try-backstage-catalog
Entity Ordering
Always use this order in the file, separated by ---:
System (exactly one per file)
API entries that are local to this product (e.g. an internal /api/revalidate provided by our web app and consumed by our worker)
Component entries (the codebases we build/deploy: web, worker, mobile, studio)
Only local entities go in your product's backstage.yaml. Shared services are referenced, not redefined — see Shared catalog references.
Local API entities are appropriate when your product exposes an HTTP surface that another component in the same product (or another TRY product) calls. They are not appropriate for documenting your callees — those are referenced through consumesApis.
Owner convention
spec.owner always resolves to a Group. In our setup, Groups come from two places:
GitHub teams under the tryhuset org — auto-imported by the githubOrg catalog provider as group:default/<gh-team-slug>. This is the source of truth.
Manually-defined groups in try-backstage-catalog/org/*.yaml — typically partner orgs or non-GitHub business units.
Always write owner as the fully-qualified group:default/<gh-team-slug> — explicit kind and namespace. Groups always live in the default namespace; spelling it out means the reference resolves correctly no matter what namespace the containing entity uses. Examples:
✅ owner: group:default/ecom-tech
✅ owner: group:default/creative-tech
⚠️ owner: group:ecom-tech (works only if the entity is also in default — fragile)
❌ owner: ecommerce (friendly name — may not resolve)
❌ owner: ecom-tech (ambiguous — could be a User)
If you don't know the GitHub team, ask the user. Do not guess.
Shared catalog references
Third-party services, hosting platforms, observability backends, and other shared infrastructure live in try-backstage-catalog (https://github.com/tryhuset/try-backstage-catalog). You do not clone, read, or otherwise check that repo. The skill has no access to it. Reference shared entities purely by the canonical name pattern:
Resource: resource:tryhuset/<service-slug> — e.g. resource:tryhuset/vercel, resource:tryhuset/cloudflare, resource:tryhuset/hyperdx
API: tryhuset/<service-slug>-api — e.g. tryhuset/storyblok-api, tryhuset/centra-api
Public API: public/<api-name> — e.g. public/udir-api
Partner-owned: resource:<org>/<name> or <org>/<name>-api — e.g. <partner-org>/<service>-api
<service-slug> is kebab-case of the service's brand name (examples: google-maps, azure-openai, aws-s3, cloudflare).
Do not redefine shared services as local entities in your product's backstage.yaml. Keep the shared-vs-product separation clean.
Because the skill does not check what's in the catalog, you can't know whether the entity exists. Reference the canonical slug regardless. If it doesn't exist there yet, Backstage will surface a temporary "entity not found" warning — a catalog admin reconciles it later (either by adding the entity, or by telling the dev to rename if the slug is non-standard). This keeps product PRs unblocked and lets the catalog grow organically.
Naming and Namespaces
Use kebab-case for all metadata.name values.
Local product entities (your System, Components, Resources, internal APIs) live in the default namespace — don't set metadata.namespace. Avoids verbose refs like acme/acme-web.
Shared services live in the tryhuset, public, or partner namespaces. You reference them; you don't define them. See Shared catalog references for the patterns.
The relation rules (most-violated, most-important)
This is the core conceptual model. Get this right and most other things follow.
Before the field rules: pick the right entity kind
API = something your code calls over the network (HTTP, GraphQL, webhooks). Reference via consumesApis. Examples: Storyblok, Centra, Mapbox, any third-party HTTP endpoint.
Resource = something your code runs on or is bound to at runtime. Reference via dependsOn. Examples: Vercel, Cloudflare, HyperDX, queues, durable objects.
For any given service, pick the one that matches what you do with it. Don't reference both for the same service — the field you're writing dictates the entity kind. If you're filling in consumesApis, you're referencing an API; if you're filling in dependsOn, you're referencing a Resource. They're not paired in product backstage.yaml files.
Internal APIs in the same product: if your worker calls your web app's /api/revalidate, model the revalidate endpoint as a local API entity and put it in the worker's consumesApis.
providesApis — who exposes the API
Use on the producing Component. Every entry must have a corresponding API entity (either local in this file or in try-backstage-catalog).
Data bindings: resource:my-product-queue (a Cloudflare Queue bound to the worker), resource:my-product-durable-object
Log drains, observability hooks
Sibling Components only when there's a real runtime data binding (rare — usually it's HTTP, so use consumesApis)
Don't duplicate edges
For a given relationship, pick one edge type:
HTTP call to a service that also has a resource? → consumesApis: tryhuset/storyblok-apionly. Don't add dependsOn: resource:tryhuset/storyblok.
Sibling component that we call over HTTP? → consumesApis: <our-local-api>only. Don't add dependsOn: component:other-component.
A second edge that encodes the same relationship is noise; it makes the graph misleading and the file harder to maintain.
Common mistake: cross-component dependsOn
If a subcomponent (GUI, mobile, studio) calls a server over HTTP, that's consumesApis of the server's API entity, not dependsOn: component:server. The dependsOn would imply a runtime data binding (shared queue, shared DB) — which is usually not what's happening.
Required Metadata
Every entity must include:
metadata.name, metadata.title, metadata.description — narrative, not boilerplate
spec.owner (as group:default/<slug>), spec.lifecycle, and spec.system (where applicable)
Annotations
The following annotations are commonly useful. Use what applies; don't include placeholders.
annotations:# Source / docsgithub.com/project-slug:tryhuset/repo-namebackstage.io/source-location:url:https://github.com/tryhuset/repo-name/tree/main/apps/webtry.no/documentation:https://...# external link to docs (Notion, Confluence, etc.)# backstage.io/techdocs-ref: dir:. # only if the repo has a mkdocs.yml and we want docs rendered IN Backstage# Project trackersjira.com/project-key:PROJbusy.no/project-id:"ACME"hubspot.com/company-id:"12345678"try.no/project-owner:"<PM name>"# Observabilitysentry.io/project-slug:try-apt-as/project-nametry.no/log-drain:hyperdx# SLA (see "Project card" below — these populate a UI block)try.no/sla-tier:standardtry.no/sla-agreement:"<agreement reference>"try.no/sla-client-domain:example.no
Strongly recommended on the System:github.com/project-slug.
Strongly recommended on each Component in a monorepo:backstage.io/source-location pointing at the subdir.
Only add backstage.io/techdocs-ref: dir:. when the repo also has a mkdocs.yml and you actually want docs rendered in Backstage. The annotation alone does nothing — without mkdocs.yml the Docs tab just sits empty.
The Project card
The Backstage entity page shows a custom "Project" card that aggregates the commercial/operational metadata above (Busy ID, Jira key, SLA, log drain, project owner, external docs link). It inherits annotations from the System down to its Components — set them once on the System and every Component shows the same values.
Annotations it reads:
busy.no/project-id — Busy time-tracking ID. The conventional value is the uppercase project key (e.g. ACME).
jira.com/project-key — Jira board key. Auto-linked to tryoslo.atlassian.net/browse/<key>.
try.no/sla-tier — SLA tier. Default to standard when generating. The CS team is the source of truth for anything other than standard — ask the user (who will check with CS) before writing a non-default value.
try.no/sla-agreement — Free-text reference to the SLA contract. CS team is the source of truth.
try.no/sla-client-domain — Production domain for the client. Usually obvious from the System's production link.
try.no/project-owner — The PM / project owner (person, not GitHub team).
try.no/documentation — External docs URL (Notion, Confluence, Storybook…). Separate from backstage.io/techdocs-ref which renders in-repo Markdown inside Backstage.
try.no/log-drain — Where logs go (e.g. hyperdx, vercel). If both Components share the same drain, set once on the System.
Put these on the System entity by default. Only override on a Component when that Component differs (e.g. web logs go to Vercel while the worker exports OTEL to HyperDX).
Links
Put metadata.links on both the System AND on each Component. Links are where developers actually click — they pay off every day. Be generous; the cost of an extra link is near-zero, the cost of a missing daily-click link is real friction.
Required System links
For every System, always include these links if you can identify the underlying service from the repo:
Link
When to include
How to derive
Production URL
Always
Ask the user if not obvious from the repo (often in vercel.json rewrites, README, or try.no/sla-client-domain).
Hosting platform project
When dependsOn: resource:tryhuset/vercel or :cloudflare is present
Vercel: https://vercel.com/<team>/<project> (team is usually tryhuset or FrendDigital — confirm from vercel.json or the preview URL slug). CF: https://dash.cloudflare.com/<account_id> from wrangler.jsonc.
CMS admin
When the product uses a CMS (Storyblok, Sanity, Contentful…)
Storyblok: https://app.storyblok.com/#/me/spaces/<SB_SPACE_ID>/dashboard — pull SB_SPACE_ID from wrangler.jsonc or .env*.
Commerce/e-commerce admin
When the product uses Centra, Shopify, etc.
Centra: https://<store>.centra.com/<admin-path> — derive <store> from CENTRA_INTEGRATION_URL in wrangler.jsonc or similar. <admin-path> is NOT a constant (it's a per-instance Centra path like ams2019, but varies between clients). Always ask the user what the Centra admin path is — do not borrow it from another TRY project.
Shape vs. values — that's the line.
What's safe to infer from vendor URL conventions (public, documented, don't change per client) — the shape of the URL:
Centra admin: https://<store>.centra.com/<admin-path>
What must be grounded in this repo (read it out of a file, don't assume) — every variable inside those URLs: the project, account ID, worker name, queue name, store slug, Centra admin path, Storyblok space ID. For each one, look in this order: wrangler.jsonc, vercel.json, .env* / .env.example, README*, deploy scripts (package.json scripts, CI workflows). If the slug isn't in any file you can read from this repo, ask the user.
House constants (don't ask, don't borrow per-project, just use these):
Vercel team: tryhuset
GitHub org: tryhuset
Required Component links
For every Component, always include the dashboard URLs that match its dependsOn:
dependsOn: resource:tryhuset/vercel → add https://vercel.com/<team>/<project> on the Component.
dependsOn: resource:tryhuset/cloudflare → add the worker dashboard URL: https://dash.cloudflare.com/<account_id>/workers/services/view/<worker_name>/production (worker name is name in wrangler.jsonc, account ID is account_id).
dependsOn: resource:<your-queue> (Cloudflare Queue) → add the queue dashboard if the worker is the producer/consumer: https://dash.cloudflare.com/<account_id>/workers/queues/<queue-name>.
Add the public production URL only on the Component that actually serves traffic (typically the web Component) — not on workers.
dependsOn: the shared platform it lives on, e.g. resource:tryhuset/cloudflare
Use providesApis only if the resource genuinely exposes an HTTP/protocol API consumers reference
API
Integration surfaces. Use sparingly in the local file — the only common case is internal APIs between sibling Components in the same product (e.g. a Next.js /api/revalidate endpoint called by a worker).
definition: an inline note pointing to method, auth model, source file. Doesn't need to be a full OpenAPI doc.
Third-party and shared internal APIs are referenced via consumesApis, not redefined locally. See Shared catalog references.
Template
Use this as the starting point. Remove sections that don't apply, add sections as needed. Keep the header comment and entity order intact.
# Backstage catalog manifest for the <Product> product.## Quick primer:# System = the product as a whole.# Component = something we build and deploy.# Resource = infrastructure we depend on.# API = an HTTP/websocket surface — third-party or one of our own.## House conventions:# - consumesApis → who calls an API (HTTP direction). Third-party AND component-to-component.# - providesApis → who exposes an API. Needs a matching API entity.# - dependsOn → platforms / runtime bindings. NOT for HTTP.# - owner → GitHub team slug, written as group:default/<slug>.## Reference catalog: https://github.com/tryhuset/try-backstage-catalog---apiVersion:backstage.io/v1alpha1kind:Systemmetadata:name:<system-name>title:<SystemTitle>description:<One-ortwo-sentencenarrativeofwhatthisproductisandwhatitrunson.>links:-url:https://www.<production>.notitle:Productionicon:web-url:https://vercel.com/tryhuset/<project>title:Vercelicon:dashboardtags:-<domain-tag>-<framework-tag>-<tech-tag>annotations:github.com/project-slug:tryhuset/<repo-name>busy.no/project-id:<UPPERCASE-KEY>try.no/sla-tier:standard# default; CS team owns non-standard valuestry.no/log-drain:hyperdx# only set on System if all Components share it# backstage.io/techdocs-ref: dir:. # add only if the repo has a mkdocs.ymlspec:owner:group:default/<gh-team-slug>lifecycle:production---# Local API — only when a sibling Component in this product calls it.# Third-party / shared APIs live in try-backstage-catalog.apiVersion:backstage.io/v1alpha1kind:APImetadata:name:<product>-<api-name>title:<APITitle>description:<Whatthisendpointdoesandwhocallsit.>spec:type:restlifecycle:productionowner:group:default/<gh-team-slug>system:<system-name>definition:|
<METHOD> /<path>
Auth: <auth model>
Source: <file path>
---apiVersion:backstage.io/v1alpha1kind:Componentmetadata:name:<component-name>title:<ComponentTitle>description:<Whatthiscomponentdoes—narrative,notboilerplate.>tags:-<tech-tags>links:-url:https://vercel.com/tryhuset/<project>title:Vercelprojecticon:dashboardannotations:backstage.io/source-location:url:https://github.com/tryhuset/<repo>/tree/main/apps/<subdir>spec:type:servicelifecycle:productionowner:group:default/<gh-team-slug>system:<system-name>providesApis:-<product>-<api-name>consumesApis:-tryhuset/<external-api>-public/<public-api>dependsOn:-resource:tryhuset/<hosting-platform>-resource:<product>-<local-resource>---apiVersion:backstage.io/v1alpha1kind:Resourcemetadata:name:<product>-<resource-name>title:<ResourceTitle>description:<Whatthisresourceisandwhatit'susedfor.>tags:-<tech-tags>spec:type:infrastructurelifecycle:productionowner:group:default/<gh-team-slug>system:<system-name>dependsOn:-resource:tryhuset/<shared-platform>
Checklist
Before finishing, verify:
Factual specifics (most important — these are the bugs that survive review):
Every HTTP method, auth env-var name, and source path written into a local API entity's definition was read out of the actual route file in this session — not borrowed from a similar product
Every Resource metadata.name either matches the literal infra binding name from wrangler.jsonc / vercel.json / equivalent, OR the real name appears in the Resource's description
Every directory under apps/* (and packages/*, services/*) was either modeled as an entity or explicitly skipped with a reason
github.com/project-slug matches the actual GitHub remote (or has been confirmed with the user if the repo was moved/mirrored)
Structural:
Header comment present, with the conventions block
System entity comes first; entity order: System → local APIs → Components → Resources
All metadata.name values use kebab-case
No metadata.namespace on local product entities (they live in default)
Shared internal services referenced as resource:tryhuset/<name>, not redefined
Public APIs referenced as public/<api-name>
owner everywhere is written as group:default/<gh-team-slug>
consumesApis used for ALL HTTP (third-party AND sibling components)
providesApis only used when a matching API entity exists
dependsOn is only platforms / runtime bindings — no HTTP relations
No relation duplicated across dependsOn and consumesApis
backstage.io/techdocs-ref: dir:. on the System only if a mkdocs.yml exists in the repo (don't promise rendering that won't happen)
backstage.io/source-location on each Component in a monorepo
System metadata.links includes the daily-click set: production URL, hosting platform project (Vercel/CF), CMS admin (if applicable), commerce admin (if applicable). Missing one is a defect, not a stylistic choice.
Each Component has the dashboard URL matching its dependsOn: Vercel project for Vercel-hosted Components, Cloudflare worker dashboard for Workers, etc.
Every entity has name, title, description, tags, owner, lifecycle
kind: values use TitleCase (System, Component, Resource, API)
File uses --- separators between entities
No duplicate entity names
At minimum: one System, one Component for the core deployable, one Resource for the hosting platform binding
Final step: hand off for human review
After writing the file, tell the user explicitly:
Generated backstage.yaml is ready. Please read it top-to-bottom before committing — the catalog records facts about ownership, infrastructure, and integrations that other people will rely on. Pay particular attention to:
Entity names and the dependsOn / consumesApis graph — does it match how the system actually works?
Real-world specifics (HTTP methods, env var names, resource binding names, source paths, account/space/queue IDs) — do they match the running system?
Owner team — is it the right team that should be paged for this product?
Annotations (Busy ID, SLA tier, Centra admin path, HubSpot ID, etc.) — are the values correct?
Edit anything that's wrong before committing. The skill makes its best inference from this repo's source, but you are the source of truth.
Do not assume silence is approval. The skill's job is to draft; the human's job is to verify.