Patterns
Six patterns covering the most common reasons teams reach for external:. Each shows the minimal Katalog snippet that makes it work and explains the key design choice.
Health gate
Gate a Deployment on an upstream service being healthy. If the health check fails, the Deployment is not created or updated — no broken app, no partial rollout.
onReconcile:
external:
- name: healthCheck
url: "{{ .spec.serviceUrl }}/health"
expectedStatus: 200
continueOnError: true # reconcile continues — phase state machine shows the failure
timeout: 5s
deployments:
- name: "{{ .metadata.name }}"
image: "{{ .spec.image }}"
when:
- field: external.healthCheck.status
equals: "200"
status:
fields:
- path: phase
value: "Degraded"
when:
- field: external.healthCheck.called
equals: "true"
- field: external.healthCheck.status
notEquals: "200"
- path: phase
value: "Ready"
when:
- field: external.healthCheck.status
equals: "200"
- field: "{{ allReplicasReady .children.deployment }}"
equals: "true"
continueOnError: true means a failed health check is visible in status rather than surfacing as a reconcile error. Use continueOnError: false when the Deployment must never exist without a passing health check — the reconcile halts and Ready=False is written on the condition.
Try it:
ork init --pack use-cases
cd external/01-health-gate
ork run
Dynamic config injection
Fetch a JSON config blob from a config server on every reconcile. Embed the response body into a ConfigMap. The Deployment mounts the ConfigMap — the app always sees current config without a pod restart.
onReconcile:
external:
- name: appConfig
url: "{{ .spec.serviceUrl }}/config/{{ .metadata.name }}"
continueOnError: true # config unavailable → keep the last written ConfigMap
timeout: 5s
configMaps:
- name: "{{ .metadata.name }}-config"
reconcile: true
data:
app.json: "{{ .external.appConfig.body }}"
when:
- field: external.appConfig.called
equals: "true"
- field: external.appConfig.error
operator: notExists
The when: condition on the ConfigMap means the config is only overwritten when the call succeeds. A transient config service outage leaves the last-written config in place — the Deployment keeps running without interruption.
Try it:
ork init --pack use-cases
cd external/02-config-inject
ork run
Image signing — “once per image change”
Call a signing service when the image changes. Gate the Deployment on the signed status. Use a status field to remember the last signed image — the call is skipped on every subsequent reconcile until the image changes again.
onReconcile:
external:
- name: signImage
url: "{{ .spec.serviceUrl }}/sign"
method: POST
body: '{"image": "{{ .spec.image }}"}'
token: "$IMAGE_SIGNING_TOKEN"
expectedStatus: 200
continueOnError: false
timeout: 15s
when:
# Skip the call when the image is already signed.
- field: status.signedImage
notEquals: "{{ .spec.image }}"
deployments:
- name: "{{ .metadata.name }}"
when:
- field: status.signedImage
equals: "{{ .spec.image }}"
status:
fields:
# Written after a successful sign — prevents re-signing on the next reconcile.
- path: signedImage
value: "{{ .spec.image }}"
when:
- field: external.signImage.status
equals: "200"
The pattern — call → write result to status → gate future calls on status — generalises to any “call once per spec change” scenario: license checks, DNS record creation, service mesh registration, cluster provisioning.
Try it:
ork init --pack use-cases
cd external/03-image-signing
ork run
Sequential chained calls
Fetch a short-lived token, then use it in the next call. The resolver is updated after every call so later calls can reference earlier results via template expressions in their url:, token:, or body: fields.
onReconcile:
external:
- name: tokenFetch
url: "{{ .spec.authUrl }}/token"
method: POST
token: "$CLIENT_SECRET"
continueOnError: false # no token = don't proceed
timeout: 5s
- name: resourceCheck
url: "{{ .spec.serviceUrl }}/resources/{{ .metadata.name }}"
token: "{{ .external.tokenFetch.body }}" # uses the previous call's result
continueOnError: true
timeout: 5s
If tokenFetch fails (with continueOnError: false), the reconcile halts and resourceCheck never runs. This is the correct behaviour — there is nothing to authenticate with.
Try it:
ork init --pack use-cases
cd external/04-chained
ork run
Conditional webhook notification
Fire a Slack or Teams webhook only when the phase transitions to a specific state. Use when: to gate the call so it does not fire on every reconcile.
onReconcile:
external:
- name: slackAlert
url: "$SLACK_WEBHOOK_URL"
method: POST
body: '{"text": "{{ .metadata.name }} is {{ .status.phase }} in {{ .metadata.namespace }}"}'
continueOnError: true
timeout: 3s
when:
- field: status.phase
equals: "Degraded"
The when: condition checks the current status before the call runs. The call fires only while the CR is in Degraded — once the phase changes, the condition fails and the call is skipped. To fire exactly once on transition (not every reconcile while degraded), add a second condition:
when:
- field: status.phase
equals: "Degraded"
- field: status.alertSent
operator: notExists
# Then write status.alertSent after a successful notify:
status:
fields:
- path: alertSent
value: "true"
when:
- field: external.slackAlert.status
equals: "200"
- path: alertSent
value: ""
clearOnFalse: true
when:
- field: status.phase
equals: "Degraded"
Feature flags and runtime toggles
Fetch feature flag state from LaunchDarkly, Unleash, or a custom flags API. Gate which resources to create on the flag value. The continueOnError: true setting ensures the Deployment keeps running even if the flags service is unavailable.
onReconcile:
external:
- name: featureFlags
url: "https://flags.internal/api/{{ .metadata.name }}"
token: "$FEATURE_FLAG_TOKEN"
continueOnError: true
timeout: 3s
deployments:
# v2 Deployment only created when the feature flag enables it.
- name: "{{ .metadata.name }}-v2"
image: "{{ .spec.imageV2 }}"
when:
- field: external.featureFlags.called
equals: "true"
- field: external.featureFlags.body
contains: '"v2Enabled":true'
The contains: operator on .body checks for a JSON substring. This avoids any JSON parsing — the operator treats the body as opaque text.