Skip to content

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.

Just deploy. The agent discovers your workload and creates an entity stub automatically.

apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
namespace: payments

No Shoehorn annotations needed. The entity gets:

FieldValueSource
Namecheckout-apiK8s metadata.name
TypeserviceInferred from Kind
Teampayments-teamNamespace label
HealthhealthyK8s runtime
Replicas3/3K8s runtime
Imageacme/checkout:v1.2.3Pod spec
Repo URLhttps://github.com/acme/checkoutArgoCD/FluxCD source (if active)

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: v1
kind: Namespace
metadata:
name: payments
labels:
team: payments-team # Agent reads this

If no namespace label exists, the agent falls back to the namespaceTeamMapping in the agent’s helm values. If neither exists, the team is unassigned.

K8s KindEntity Type
Deploymentservice
StatefulSetservice
DaemonSetresource
CronJobservice
Jobservice

Override with shoehorn.dev/type annotation if the default is wrong.


Add shoehorn.dev/* annotations to your deployment manifest or helm values for simple metadata. These are short strings - a few bytes each.

apiVersion: apps/v1
kind: Deployment
metadata:
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"
AnnotationTypeExample
shoehorn.dev/teamstringpayments-team
shoehorn.dev/tierstringgold, silver, bronze
shoehorn.dev/lifecyclestringproduction, staging, deprecated
shoehorn.dev/typestringservice, database, resource
shoehorn.dev/tagscsvapi,payments,critical
shoehorn.dev/slackChannelstring#payments-alerts
shoehorn.dev/descriptionstringCheckout 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 secrets
  • control-plane.alpha.kubernetes.io/* — internal control plane metadata

Additionally, individual annotation values are truncated to 1KB to prevent payload bloat.

Instead of adding annotations to every workload, use Kyverno to propagate them automatically. See Kyverno Examples for ready-to-use policies.


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/v1
kind: Deployment
metadata:
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.

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"

Only include the fields you need. Everything is optional except schemaVersion and service.id/service.name.

.shoehorn/catalog.yaml
schemaVersion: 1
service:
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.yaml

For the full manifest schema, see the Manifest Reference.

If multiple workloads in a repo share one manifest file, use a fragment selector to pick the right service:

# Workload A
annotations:
shoehorn.dev/entityFile: ".shoehorn/platform.yaml#checkout-api"
# Workload B
annotations:
shoehorn.dev/entityFile: ".shoehorn/platform.yaml#payment-worker"

The manifest file contains multiple services:

.shoehorn/platform.yaml
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-api

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.

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

Modes 2 and 3 work together. Annotations override manifest values.

production/values.yaml
annotations:
shoehorn.dev/tier: "gold"
shoehorn.dev/slackChannel: "#payments-prod"
shoehorn.dev/entityFile: ".shoehorn/catalog.yaml"
# staging/values.yaml
annotations:
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)


On the entity detail page, the Definition tab shows a resolved YAML view with the source of each field:

name: checkout-api # source: k8s
type: service # source: inferred (Kind=Deployment)
team: payments-team # source: namespace
tier: gold # source: annotation
lifecycle: production # source: entityFile
relations: # source: entityFile
- type: depends_on
target: service:postgres

Toggle between the provenance view and the raw manifest content.


SituationRecommended Mode
Just want workloads visible in catalogMode 1 (zero config)
Need team, tier, tagsMode 2 (annotations, optionally via Kyverno)
Need relations, links, runbooks, interfacesMode 3 (entityFile)
Different metadata per environmentMode 2 + 3 (annotations override manifest)
Monorepo with multiple servicesMode 3 with fragment selector
Already using Terraform provider or crawlerProbably don’t need any K8s annotations