Skip to main content

Service catalog

The catalog is orun's answer to "what exists, who owns it, how is it connected, and where is it running?" — derived entirely from the sources you already declare. It is not a database you curate: it is a projection of component.yaml files, the intent, CODEOWNERS, the resolved composition sources, and recorded execution history. If the sources change, the catalog changes; if something is in the catalog, a source put it there.

That makes the catalog the platform's standards, observed, and it is what coding agents read to understand a repository: the catalog tools on orun mcp and orun catalog affected answer "what is this, who owns it, and what does my change touch?" from the same declarations the planner uses. See agents in orun repositories.

component.yaml · intent.yaml · CODEOWNERS · composition sources
│ resolve
▼
┌──────────────────────────────────────┐
│ catalog @ source snapshot │
│ entities/<Kind>/… (orun.io/v1) │
│ relations.json (typed edges) │
│ indexes (impact, refs) │
└──────────────────────────────────────┘
│ read
▼
orun catalog list · describe · tree · affected · cockpit
+
live plane (deployments · executions),
derived on read from execution history

This page covers the entity model. For the on-disk mechanics see the state model; for the commands see orun catalog.

Entities and kinds​

The catalog is multi-kind. Components are the authored core, and around them the resolver derives the rest of the organizational picture:

KindWhat it representsDerived from
ComponentA deployable/operable unitcomponent.yaml, resolved and enriched
SystemA set of components delivering a capabilityspec.system declarations
DomainA policy/ownership areaComponent domains and groups in the intent
GroupAn owning teamOwnership refs (group:<name>)
APIA provided/consumed interfaceComponent contracts and composition effects
ResourceInfrastructure a component relies onComponent declarations and composition effects
EnvironmentA runtime context as a catalog citizenThe intent's environment matrix
CompositionA golden path, versioned and lifecycle-stagedThe composition lock
orun catalog list # components (the default)
orun catalog list --kind System # any other kind
orun catalog list --kind Environment
orun catalog describe api-edge-worker # the full envelope for one entity
orun catalog tree api-edge-worker --direction both

Derived entities are not stubs. A System knows its members (spec.members, surfaced as a MEMBERS count in list output); an Environment knows what is deployed to it; a Composition knows which components ride its golden path.

Entity keys follow one grammar across kinds — <namespace>/<repo>/<name> for component-like entities, group:<name> / user:<name> for owners — so a relation edge always points at exactly one thing.

The kind is not part of the key, so names must not repeat across kinds within a repo. A domain named after a component (spec.domain: web on the web component) gives both the key default/<repo>/web. When an entity of another kind takes a component's key, the catalog keeps the Component and leaves the other entity out. orun plan warns about it and orun catalog validate reports it as an error. Rename the domain (or system, environment, …) to fix it.

The entity envelope​

Every entity, authored or derived, resolves into the same orun.io/v1 envelope. orun catalog describe renders it in full; --json emits it machine-readable:

SectionContents
metadataName, title, description, labels, tags
ownershipOwner (typed ref), system, domain — with provenance
lifecycleStage: experimental · production · deprecated · retired
specKind-specific declaration (type, environments, parameters, members…)
relationsTyped edges to other entities
contractsAPIs provided and consumed
integrationsJoin keys for external platforms (Datadog, PagerDuty, …)
docs / linksDocumentation pointers and external URLs
extensionsNamespaced vendor blocks (x-<vendor>), preserved verbatim
provenanceWhere each fact came from: manifest hash, resolver version, inference trail

The envelope is the catalog's contract: portals, scripts, and the cockpit all read the same shape regardless of kind.

Ownership, with provenance​

Every entity has an owner, and the catalog tells you how it knows:

  1. An authored spec.owner always wins — ownership.source: authored.
  2. Otherwise the resolver reads the workspace CODEOWNERS file and applies the last matching rule — ownership.source: CODEOWNERS.
  3. Derived entities inherit ownership from their members — ownership.source: inherited.

This means ownership is never a second bookkeeping system. The file your code review process already enforces is the file the catalog reads, and the provenance field keeps the derivation honest.

The relation graph​

All connections in the catalog live in one typed graph (relations.json): dependsOn, partOf, ownedBy, providesApi, consumesApi, runsOn, deployedTo, composedBy — with inverse edges materialized on read.

One graph, two consumers:

  • Catalog reads — orun catalog tree walks it; describe renders an entity's edges; portals query it.
  • Change detection — the same engine behind --changed and orun catalog affected consumes the same edges.

That convergence is the point: "what depends on this component?" and "what should this pull request run?" are the same question asked by different tools, so they get the same answer.

Compositions in the catalog​

Golden paths are entities too. A composition carries a semver version and a lifecycle stage (stable · beta · deprecated), appears in the catalog with composes relations to the components that ride it, and may declare effects — what running it contributes:

# composition.yaml (authored by the platform team)
spec:
version: 2.3.0
lifecycle: stable
effects:
integrations:
cloudflare: { product: workers }
provides:
- default/orun/cloudflare-edge # → Resource entity
exposes:
- default/orun/edge-http-api # → API entity
scorecards:
satisfies: [has-deploy-pipeline]

A component that adopts this golden path gets its integrations, Resources, and APIs in the catalog for free — declared once by the platform team, not copy-pasted into every component.yaml. Pinning a deprecated composition version surfaces a warning at resolve time, where it can actually change behavior.

Effects are authored intent. What actually happened is the live plane's job.

The live plane​

Deployments, latest executions, and health are derived on read from the recorded execution history — the same objects orun status reads. They are never persisted into catalog blobs, so they can never be stale relative to the runs that produced them. orun catalog describe appends them to the envelope:

Live deployments
production rev cat-2a2cd793 · succeeded · 2026-06-10T14:02Z

This is the catalog's desired/observed split: the envelope says what the sources declare; the live plane says what the record shows.

Where the catalog looks​

The catalog is built from the component manifests the planner plans, so the two read the same scope from intent.yaml:

  • discovery.roots bounds the walk. Each root is a directory relative to the intent file, scanned recursively for component.yaml (or component.yml). With no roots declared, the whole workspace is scanned. A root outside the workspace is not scanned by the catalog (catalog paths are workspace-relative), and a root that does not exist is an error, as it is for the planner. When the intent inherits presets with extends:, presets may contribute roots of their own, so the catalog keeps the whole-workspace scan rather than hiding components the planner plans.
  • catalog.discovery.exclude prunes directories inside the roots by basename, at any depth, on top of the built-in set (.git, .orun, build, dist, node_modules, vendor). Use it for copies that are not components of their own, such as fixtures or overlay variations.
discovery:
roots: [apps/, infra/]
catalog:
discovery:
exclude: [fixtures, variations]

Every manifest discovery finds must declare a unique metadata.name. Two files with the same name are a resolve error that names both files; orun plan --changed, orun run --changed, and orun catalog refresh stop on it.

Catalog identity and snapshots​

A catalog is content-addressed: resolving the same sources yields the same catalog id, no matter which command did the resolving (orun plan, orun catalog refresh, the cockpit's auto-refresh). Snapshots are immutable; named refs (catalogs/current, branches/<name>, prs/<n>) select them, and orun catalog diff compares any two. History costs almost nothing — identical content is stored once.

Adopting the v1 model​

The richer authoring surface (system, lifecycle, integrations, links, docs, extensions) is opt-in and additive. Three guarantees make adoption safe:

  • Old catalogs read as new. Snapshots written under the earlier v1alpha1 model stay byte-identical on disk and up-convert to the v1 envelope on read. No migration step, no flag.
  • orun catalog migrate is advisory. It lints authored component.yaml files for v1 readiness — a legacy apiVersion, a missing owner, a missing lifecycle stage, a missing system — and prints per-file findings. It never rewrites a file.
  • Un-migrated components still resolve. A bare-minimum manifest is a valid catalog entity; enrichment only adds.
orun catalog migrate # what would make each component a first-class v1 entity?
orun catalog migrate --json # the structured result, for CI

Design notes​

Why the catalog works the way it does:

  • Derived, not curated. Catalogs maintained by hand drift the day they ship. This one is a pure function of sources + records, so it is exactly as current as your repository.
  • Same envelope, every kind. One schema for consumers means a portal, a script, and the cockpit never special-case entity types.
  • Provenance everywhere. Every derived fact says where it came from. A catalog you can't audit is a rumor.
  • Live state is a projection. Persisting "what's deployed" would create a second source of truth; deriving it from sealed executions keeps one.

See also​