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://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/registration.md

Registration in app.go

How to wire your reconciler into the app's New function.

func New(cfg app.Config) (app.App, error) {
    cfg.KubeConfig.APIPath = "/apis"

    // 1. Build a client for the kind your reconciler operates on
    client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).
        ClientFor(mykindv1alpha2.MyKindKind())
    if err != nil {
        return nil, fmt.Errorf("creating client: %w", err)
    }

    // 2. Register the kind with reconciler attached to the latest version only
    a, err := simple.NewApp(simple.AppConfig{
        Name:       "my-app",
        KubeConfig: cfg.KubeConfig,
        ManagedKinds: []simple.AppManagedKind{
            {
                // Older version: validation + mutation only
                Kind:      mykindv1alpha1.MyKindKind(),
                Validator: NewValidator(),
                Mutator:   NewMutator(),
            },
            {
                // Latest version: validation + mutation + reconciler
                Kind:       mykindv1alpha2.MyKindKind(),
                Reconciler: NewMyKindReconciler(client),
                Validator:  NewValidator(),
                Mutator:    NewMutator(),
            },
        },
    })
    if err != nil {
        return nil, fmt.Errorf("error creating app: %w", err)
    }

    // 3. Validate the AppManifest matches what's registered (catches missing route handlers etc.)
    if err = a.ValidateManifest(cfg.ManifestData); err != nil {
        return nil, fmt.Errorf("app manifest validation failed: %w", err)
    }
    return a, nil
}

Why attach the reconciler only to the latest version

With multi-version kinds, the reconciler should only fire on the canonical/latest version. The SDK auto-converts incoming objects via the conversion webhook, so reconciling v1alpha1 and v1alpha2 separately would double-reconcile the same resource.

Common failure modes

SymptomLikely cause
app manifest validation failed: route handler missingA routes: entry in CUE has no Go handler registered. Add app.RegisterCustomRouteHandler("<routeName>", handler) before ValidateManifest.
Reconciler doesn't fire on resource createsThe reconciler is attached to the wrong version, or UsePlain: true was set on BasicReconcileOptions and finalizers aren't being managed.
error creating client: ...Wrong API path or kube config — check cfg.KubeConfig.APIPath = "/apis" is set before building the client.

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 or Grafana Labs.

Download ZIP

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