reconciler-logic — Claude Code skill: what it does, how to install
Helps implement reconcilers and watchers for grafana-app-sdk applications. Guides reconcile loops, atomic status updates, and resource filters.
npx skills add grafana/skills --skill reconciler-logic -a claude-codeWhat reconciler-logic does
The skill provides implementation patterns for async business logic in grafana-app-sdk projects. It guides you through writing operator.TypedReconciler handlers that react to resource creation, update, and deletion events. It also details generation-based skips via LastObservedGeneration and delayed retries using RequeueAfter.
Status writes require resource.UpdateObject instead of client.Update. Using plain client.Update sends the entire object and conflicts with concurrent spec edits, resulting in 409 Conflict errors.
The instructions also cover registering controllers inside app.go. You configure BasicReconcileOptions with LabelFilters and FieldSelectors to target matching resources, control namespace scope, and let OpinionatedReconciler manage finalizers automatically.
What it helps with
- implement typed reconcile loops for grafana-app-sdk resources
- prevent 409 Conflict errors during status updates with resource.UpdateObject
- filter reconcile events using label and field selectors in BasicReconcileOptions
- wire reconcilers and operator controllers into app.go
Example prompts for the agent
- Write a TypedReconciler for MyKind with atomic status updates via resource.UpdateObject
- How do I configure BasicReconcileOptions to filter by env=prod and register the reconciler in app.go?
- Implement a generation skip check using LastObservedGeneration and schedule a resync with RequeueAfter
How to install reconciler-logic
1Skills CLI (Vercel)
npx skills add grafana/skills --skill reconciler-logic -a claude-code2Manually: put the files in place
~/.claude/skills/reconciler-logic/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/reconciler-logic/
1Skills CLI (Vercel)
npx skills add grafana/skills --skill reconciler-logic -a codex2Manually: put the files in place
.agents/skills/reconciler-logic/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add grafana/skills --skill reconciler-logic -a cursor2Manually: put the files in place
.cursor/skills/reconciler-logic/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install grafana/skills reconciler-logic2Manually: put the files in place
.github/skills/reconciler-logic/SKILL.md1Skills CLI (Vercel)
npx skills add grafana/skills --skill reconciler-logic -a gemini-cli2Platform CLI
gemini skills install https://github.com/grafana/skills --path skills/grafana-app-sdk/reconciler-logicManually: put the files in place
.devin/skills/reconciler-logic/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add grafana/skills --skill reconciler-logic -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about reconciler-logic
How do I install reconciler-logic in Claude Code?
Run npx skills add grafana/skills --skill reconciler-logic -a claude-code in your terminal. Claude Code will load the skill automatically.
What does reconciler-logic do?
It guides the creation of Go operator logic for grafana-app-sdk, handling resource lifecycles, watchers, and safe state synchronization.
Why use resource.UpdateObject instead of client.Update for status updates?
resource.UpdateObject fetches the latest resource version before updating the status subresource. In contrast, client.Update sends the entire object and triggers 409 Conflict errors during concurrent spec edits.
Source text
SKILL.md · Apache-2.0
---
name: reconciler-logic
license: Apache-2.0
description: Implement reconcilers and watchers for grafana-app-sdk apps — write `TypedReconciler[*MyKind]` reconcile functions, apply generation-based skip patterns, do conflict-safe status updates via `resource.UpdateObject`, configure `BasicReconcileOptions` (namespace, label/field filters, finalizer management), use `Watcher` for event-style handling, reconcile `UnmanagedKinds` (resources your app doesn't own), and register the whole thing in `app.go`. Use when writing a reconciler, implementing the reconcile loop, adding async business logic, handling create/update/delete events, processing resource state changes, scheduling periodic resyncs with `RequeueAfter`, picking between Watcher and Reconciler, or wiring a controller into `app.go` — even when the user says "process this resource", "handle X events", or "write a controller" without saying "reconciler".
---
# Reconciler Logic
Reconcilers are the async business-logic layer of a grafana-app-sdk app. The SDK enqueues a reconcile event when a resource is created, updated, or deleted; the reconciler observes the current state and drives the system toward the desired state.
## Common Workflows
### Implementing a new reconciler end-to-end
```bash
# 1. Generate operator stubs for a standalone app
grafana-app-sdk project component add operator
# 2. Implement the ReconcileFunc — see § TypedReconciler below for the pattern
# 3. Register the reconciler in app.go (see references/registration.md)
# 4. Generate, build, and verify it runs
grafana-app-sdk generate
go build ./...
go run ./cmd/operator # tail logs — reconcile entries should appear when you kubectl-apply a resource
```
If the operator starts but no reconcile events fire when you create a resource:
- Check `BasicReconcileOptions.Namespace` matches the resource's namespace
- Check `BasicReconcileOptions.LabelFilters` / `FieldSelectors` — most "no events" issues are filter mismatches (`kubectl get <resource> -o yaml` to see labels)
- Confirm the reconciler was attached to the right (latest) version of the kind
## TypedReconciler — preferred pattern
`operator.TypedReconciler` handles type assertion and provides a strongly-typed `ReconcileFunc`:
```go
type MyKindReconciler struct {
operator.TypedReconciler[*v1alpha1.MyKind]
client resource.Client
}
func NewMyKindReconciler(client resource.Client) *MyKindReconciler {
r := &MyKindReconciler{client: client}
r.ReconcileFunc = r.reconcile // wire the typed func
return r
}
func (r *MyKindReconciler) reconcile(
ctx context.Context,
req operator.TypedReconcileRequest[*v1alpha1.MyKind],
) (operator.ReconcileResult, error) {
obj := req.Object
// Skip if already reconciled this generation
if obj.GetGeneration() == obj.Status.LastObservedGeneration &&
req.Action != operator.ReconcileActionDeleted {
return operator.ReconcileResult{}, nil
}
log := logging.FromContext(ctx).With("name", obj.GetName(), "namespace", obj.GetNamespace())
log.Info("reconciling", "action", operator.ResourceActionFromReconcileAction(req.Action))
if req.Action == operator.ReconcileActionDeleted {
return operator.ReconcileResult{}, nil
}
// ... business logic ...
// Atomic status update — see § Status updates below
_, err := resource.UpdateObject(ctx, r.client, obj.GetStaticMetadata().Identifier(),
func(obj *v1alpha1.MyKind, _ bool) (*v1alpha1.MyKind, error) {
obj.Status.LastObservedGeneration = obj.GetGeneration()
obj.Status.State = "Ready"
return obj, nil
},
resource.UpdateOptions{Subresource: "status"},
)
return operator.ReconcileResult{}, err
}
```
`ReconcileAction` values: `ReconcileActionCreated`, `ReconcileActionUpdated`, `ReconcileActionDeleted`, `ReconcileActionResynced`.
To requeue after a delay (e.g. polling an external system):
```go
return operator.ReconcileResult{RequeueAfter: 10 * time.Second}, nil
```
## Status updates with `resource.UpdateObject`
Always use `resource.UpdateObject` for status writes — it fetches the latest version before applying your update function, avoiding `409 Conflict` errors when multiple reconcile events race:
```go
_, err := resource.UpdateObject(ctx, r.client, identifier,
func(obj *v1alpha1.MyKind, exists bool) (*v1alpha1.MyKind, error) {
obj.Status.LastObservedGeneration = obj.GetGeneration()
obj.Status.State = "Ready"
obj.Status.Message = ""
return obj, nil
},
resource.UpdateOptions{Subresource: "status"},
)
```
Do **not** use `client.Update` for status — it sends the full object and races with spec changes made by users.
## Generation-based skip
Check `LastObservedGeneration` at the top of the reconcile function to avoid re-processing unchanged resources:
```go
if obj.GetGeneration() == obj.Status.LastObservedGeneration {
return operator.ReconcileResult{}, nil
}
```
## `ReconcileOptions`
Control informer behavior via `BasicReconcileOptions` on the `AppManagedKind` entry:
```go
{
Kind: mykindv1alpha1.MyKindKind(),
Reconciler: reconciler,
ReconcileOptions: simple.BasicReconcileOptions{
Namespace: "my-namespace", // watch one namespace; default is all
LabelFilters: []string{"env=prod"}, // only reconcile matching resources
FieldSelectors: []string{"status.phase=Running"},
UsePlain: false, // false = wrap in OpinionatedReconciler (default; manages finalizers)
},
},
```
`UsePlain: false` (the default) wraps your reconciler in `OpinionatedReconciler`, which manages finalizers automatically so the SDK can guarantee clean deletion.
## References
- [`references/watchers.md`](references/watchers.md) — `Watcher` alternative (event-style Add/Update/Delete callbacks) + decision matrix for watcher vs reconciler
- [`references/unmanaged-kinds.md`](references/unmanaged-kinds.md) — `UnmanagedKinds` for reconciling resources your app doesn't own, with `UseOpinionated: false` guidance and common failure modes
- [`references/registration.md`](references/registration.md) — full `app.go` wiring (client setup, multi-version registration, `ValidateManifest`) + common failure modes
## External resources
- [grafana-app-sdk GitHub](https://github.com/grafana/grafana-app-sdk)
- [operator package docs](https://pkg.go.dev/github.com/grafana/grafana-app-sdk/operator)
© grafana · Apache-2.0 · grafana/skills