Generated banner for Reconciler Logic
Generated logo for Reconciler Logic

Reconciler Logic

Grafana LabsOfficialPublished by Grafana Labs

  • Skill
  • Tool integration
  • Cloud and DevOps
  • Apache-2.0
  • No issues found

From the author

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".
— the description by Grafana Labs

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides the patterns and procedures for implementing the asynchronous business-logic layer in grafana-app-sdk applications. It covers the creation of TypedReconciler functions, managing resource state changes, handling event-driven updates, and configuring informer options. It emphasizes safe status updates using resource.UpdateObject to prevent race conditions and provides guidance on managing finalizers and periodic resyncs to ensure system consistency.

What it does

  • Defines TypedReconciler patterns for strongly-typed resource processing.
  • Implements conflict-safe status updates using resource.UpdateObject.
  • Configures BasicReconcileOptions for namespace, label, and field filtering.
  • Manages resource finalizers for clean deletion of external dependencies.
  • Schedules periodic resyncs and async polling using RequeueAfter.
  • Wires controllers into the application registration process.

When to use it

  • When building the core business logic for a custom Kubernetes operator.
  • When you need to reconcile resources that your application does not own.
  • When handling complex create, update, or delete event cycles.
  • When you need to poll external systems asynchronously and update resource status.
  • When you need to filter which resources trigger reconciliation events.

Advantages

  • Prevents 409 conflict errors during status updates.
  • Reduces unnecessary processing with generation-based skip patterns.
  • Provides a structured, type-safe approach to operator development.
  • Simplifies finalizer management through opinionated defaults.

Usage examples

“How do I implement a reconciler for my custom resource MyKind?”
You define a struct embedding operator.TypedReconciler[*v1alpha1.MyKind], wire a ReconcileFunc to handle the logic, and register it in app.go using AppManagedKind.
“How can I avoid 409 conflicts when updating status?”
Use the resource.UpdateObject function, which fetches the latest object version before applying your update, ensuring atomic status changes.
“How do I make my reconciler poll an external system every 30 seconds?”
Return an operator.ReconcileResult with the RequeueAfter field set to 30 * time.Second within your reconcile function.
“How do I filter events so my reconciler only processes resources in the prod namespace?”
Configure the BasicReconcileOptions with the Namespace field set to 'prod' when registering the reconciler in your app configuration.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/grafana/reconciler-logic/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip reconciler-logic.zip -d .agents/skills

Documentation by Grafana Labs

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
# 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:

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):

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:

_, 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:

if obj.GetGeneration() == obj.Status.LastObservedGeneration {
    return operator.ReconcileResult{}, nil
}

ReconcileOptions

Control informer behavior via BasicReconcileOptions on the AppManagedKind entry:

{
    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 — Watcher alternative (event-style Add/Update/Delete callbacks) + decision matrix for watcher vs reconciler

  • references/unmanaged-kinds.md — UnmanagedKinds for reconciling resources your app doesn't own, with UseOpinionated: false guidance and common failure modes

  • references/registration.md — full app.go wiring (client setup, multi-version registration, ValidateManifest) + common failure modes

External resources

Files

references/unmanaged-kinds.md

UnmanagedKinds — watching kinds your app doesn't own

To reconcile a kind your app doesn't own (e.g. a ConfigMap, or a kind owned by another app), use UnmanagedKinds in AppConfig:

UnmanagedKinds: []simple.AppUnmanagedKind{
    {
        Kind:       corev1.ConfigMapKind(),
        Reconciler: &ConfigMapReconciler{},
        ReconcileOptions: simple.UnmanagedKindReconcileOptions{
            Namespace:      "my-namespace",
            LabelFilters:   []string{"app=my-app"},
            UseOpinionated: false,  // don't add finalizers to unmanaged resources
        },
    },
},

Why UseOpinionated: false for unmanaged kinds

The OpinionatedReconciler adds finalizers so the SDK can guarantee clean deletion. For managed kinds that's correct — the app owns the resource lifecycle. For unmanaged kinds (resources owned by another app or the cluster itself), adding finalizers is invasive: it makes deletion of the resource dependent on your app being healthy, which can leave orphan finalizers if your app is uninstalled while resources exist.

Default to UseOpinionated: false for UnmanagedKinds unless you genuinely need finalizer-driven cleanup for the unmanaged resource.

Common failure modes

SymptomLikely cause
Reconciler doesn't fireLabel filter doesn't match the resource's labels (check kubectl get <resource> -o yaml for labels)
Permission errors at startupYour app's ServiceAccount lacks RBAC on the unmanaged kind — add a Role binding for get/list/watch on the kind's API group
Resources stuck in deletionUseOpinionated: true was set; finalizer wasn't removed before app uninstall. Patch the resource to remove the finalizer manually.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Grafana Labs
Publisher
Grafana Labs (Official)
Source
grafana/skills · skills/grafana-app-sdk/reconciler-logic · commit 8fb72a5
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Grafana Labs.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download reconciler-logic.zip