Entity Enrichment Modes
The Shoehorn K8s Agent discovers workloads and creates entities in the service catalog. You control how much metadata those entities get through three enrichment modes.
Mode 1: Zero Config
Section titled “Mode 1: Zero Config”Just deploy. The agent discovers your workload and creates an entity stub automatically.
apiVersion: apps/v1kind: Deploymentmetadata: name: checkout-api namespace: paymentsNo Shoehorn annotations needed. The entity gets:
| Field | Value | Source |
|---|---|---|
| Name | checkout-api | K8s metadata.name |
| Type | service | Inferred from Kind |
| Team | payments-team | Namespace label |
| Health | healthy | K8s runtime |
| Replicas | 3/3 | K8s runtime |
| Image | acme/checkout:v1.2.3 | Pod spec |
| Repo URL | https://github.com/acme/checkout | ArgoCD/FluxCD source (if active) |
How team is inferred
Section titled “How team is inferred”The agent reads the namespace’s team label (configurable via agent.namespaceTeamLabel in helm values). If your org enforces this through policy (e.g. Kyverno), the label is already present and no further config is needed.
apiVersion: v1kind: Namespacemetadata: name: payments labels: team: payments-team # Agent reads thisIf no namespace label exists, the agent falls back to the namespaceTeamMapping in the agent’s helm values. If neither exists, the team is unassigned.
How type is inferred
Section titled “How type is inferred”| K8s Kind | Entity Type |
|---|---|
| Deployment | service |
| StatefulSet | service |
| DaemonSet | resource |
| CronJob | service |
| Job | service |
Override with shoehorn.dev/type annotation if the default is wrong.
Mode 2: Enrich with Annotations
Section titled “Mode 2: Enrich with Annotations”Add shoehorn.dev/* annotations to your deployment manifest or helm values for simple metadata. These are short strings - a few bytes each.
apiVersion: apps/v1kind: Deploymentmetadata: name: checkout-api namespace: payments annotations: shoehorn.dev/team: "payments-team" shoehorn.dev/tier: "gold" shoehorn.dev/lifecycle: "production" shoehorn.dev/tags: "api,payments,critical" shoehorn.dev/slackChannel: "#payments-alerts" shoehorn.dev/description: "Checkout API service"Available annotations
Section titled “Available annotations”| Annotation | Type | Example |
|---|---|---|
shoehorn.dev/team | string | payments-team |
shoehorn.dev/tier | string | gold, silver, bronze |
shoehorn.dev/lifecycle | string | production, staging, deprecated |
shoehorn.dev/type | string | service, database, resource |
shoehorn.dev/tags | csv | api,payments,critical |
shoehorn.dev/slackChannel | string | #payments-alerts |
shoehorn.dev/description | string | Checkout API service |
All are optional. Each overrides the inferred value from Mode 1.
For the full list of supported annotations (links, interfaces, etc.), see the Annotations Reference.
Security note: The agent strips certain annotations before pushing to the Shoehorn API to prevent accidental secret leakage. The following annotations are removed:
kubectl.kubernetes.io/last-applied-configuration— may contain environment variable values including secretscontrol-plane.alpha.kubernetes.io/*— internal control plane metadataAdditionally, individual annotation values are truncated to 1KB to prevent payload bloat.
Using Kyverno for propagation
Section titled “Using Kyverno for propagation”Instead of adding annotations to every workload, use Kyverno to propagate them automatically. See Kyverno Examples for ready-to-use policies.
Mode 3: Enrich with Manifest File
Section titled “Mode 3: Enrich with Manifest File”For structured metadata that doesn’t belong in K8s annotations - relations, links, runbooks, licenses, interfaces - point to a manifest file in your repo with a single annotation.
apiVersion: apps/v1kind: Deploymentmetadata: name: checkout-api namespace: payments annotations: shoehorn.dev/entityFile: ".shoehorn/catalog.yaml"The Shoehorn API fetches the file server-side and enriches the entity. The file is never stored in K8s - it lives in your repo, version controlled, reviewed in PRs.
Path formats
Section titled “Path formats”Relative path (recommended) - resolved against the repo URL known from ArgoCD, FluxCD, or the crawler:
shoehorn.dev/entityFile: ".shoehorn/catalog.yaml"Full URL - for files in a different repo or location:
shoehorn.dev/entityFile: "https://github.com/acme/shared-configs/blob/main/.shoehorn/checkout.yaml"Example manifest file
Section titled “Example manifest file”Only include the fields you need. Everything is optional except schemaVersion and service.id/service.name.
schemaVersion: 1service: id: checkout-api name: Checkout API type: service
relations: - type: depends_on target: service:postgres - type: consumes_api target: service:payment-provider
links: - name: Grafana url: https://grafana.internal/d/checkout icon: Grafana - name: API Docs url: https://api.acme.com/checkout/docs icon: API
integrations: runbooks: - title: Restart Procedure path: runbooks/restart.md severity: high changelog: path: CHANGELOG.md licenses: - title: Datadog vendor: Datadog expires: "2027-01-01"
interfaces: http: baseUrl: https://api.acme.com/checkout openapi: openapi.yamlFor the full manifest schema, see the Manifest Reference.
Multi-service manifests
Section titled “Multi-service manifests”If multiple workloads in a repo share one manifest file, use a fragment selector to pick the right service:
# Workload Aannotations: shoehorn.dev/entityFile: ".shoehorn/platform.yaml#checkout-api"
# Workload Bannotations: shoehorn.dev/entityFile: ".shoehorn/platform.yaml#payment-worker"The manifest file contains multiple services:
services: - schemaVersion: 1 service: id: checkout-api name: Checkout API type: service relations: - type: depends_on target: service:postgres
- schemaVersion: 1 service: id: payment-worker name: Payment Worker type: service relations: - type: depends_on target: service:checkout-apiEnrichment status
Section titled “Enrichment status”The entity detail page shows the entityFile sync status:
- Synced - manifest fetched and applied successfully
- Pending - entityFile set but not yet fetched
- Error - fetch or parse failed (hover for details)
If a fetch fails, the entity keeps its existing data. Errors are surfaced on the entity header so you can fix the file path or manifest syntax.
Change detection
Section titled “Change detection”The API caches the manifest by SHA. It re-fetches when:
- The entityFile annotation URL changes
- Periodically (every 6 hours by default)
- The crawler detects a change in the linked repository
Combining Modes
Section titled “Combining Modes”Modes 2 and 3 work together. Annotations override manifest values.
annotations: shoehorn.dev/tier: "gold" shoehorn.dev/slackChannel: "#payments-prod" shoehorn.dev/entityFile: ".shoehorn/catalog.yaml"
# staging/values.yamlannotations: shoehorn.dev/tier: "silver" shoehorn.dev/slackChannel: "#payments-staging" shoehorn.dev/entityFile: ".shoehorn/catalog.yaml"Same manifest file for relations, links, and runbooks. Different tier and Slack channel per environment.
Priority: Annotation > Manifest file > Inferred (Mode 1)
Provenance
Section titled “Provenance”On the entity detail page, the Definition tab shows a resolved YAML view with the source of each field:
name: checkout-api # source: k8stype: service # source: inferred (Kind=Deployment)team: payments-team # source: namespacetier: gold # source: annotationlifecycle: production # source: entityFilerelations: # source: entityFile - type: depends_on target: service:postgresToggle between the provenance view and the raw manifest content.
Which mode should I use?
Section titled “Which mode should I use?”| Situation | Recommended Mode |
|---|---|
| Just want workloads visible in catalog | Mode 1 (zero config) |
| Need team, tier, tags | Mode 2 (annotations, optionally via Kyverno) |
| Need relations, links, runbooks, interfaces | Mode 3 (entityFile) |
| Different metadata per environment | Mode 2 + 3 (annotations override manifest) |
| Monorepo with multiple services | Mode 3 with fragment selector |
| Already using Terraform provider or crawler | Probably don’t need any K8s annotations |