Skip to content
Back to skills

Reconciler Logic

ASecurity

Use when the user asks to "write a reconciler", "implement a reconciler", "add business logic", "handle resource changes", "process resource events", "implement the reconcile loop", "add async processing", "write a controller", "handle create/update/delete events", "use TypedReconciler", "use a Watcher", or asks how to respond to resource state changes in a grafana-app-sdk app. Provides guidance on implementing reconciler and watcher business logic for grafana-app-sdk apps.

  • 268 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added June 11, 2026
businessgobashgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned June 11, 2026

npx -y skills add grafana/skills --skill reconciler-logic --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Reconciler Logic?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Reconciler Logic
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/grafana-reconciler-logic/badge)](https://www.skillsdirectory.com/skills/grafana-reconciler-logic)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: reconciler-logic
license: Apache-2.0
description: Use when the user asks to "write a reconciler", "implement a reconciler", "add business logic", "handle resource changes", "process resource events", "implement the reconcile loop", "add async processing", "write a controller", "handle create/update/delete events", "use TypedReconciler", "use a Watcher", or asks how to respond to resource state changes in a grafana-app-sdk app. Provides guidance on implementing reconciler and watcher business logic for grafana-app-sdk apps.
---

# Reconciler Logic

Reconcilers provide the asynchronous business logic layer of a grafana-app-sdk app. When a resource is created, updated, or deleted, the SDK enqueues a reconcile event. The reconciler's job is to observe the current state of the resource and take whatever actions are needed to drive the system toward the desired state.

Reconcilers run asynchronously after a resource has been persisted — they are distinct from admission handlers, which run synchronously on ingress.

## Getting Stubs

For standalone apps, generate reconciler stubs with:

```bash
grafana-app-sdk project component add operator
```

## TypedReconciler — Preferred Pattern

The preferred implementation uses `operator.TypedReconciler`, which 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))

    // Handle deletion
    if req.Action == operator.ReconcileActionDeleted {
        return operator.ReconcileResult{}, nil
    }

    // ... business logic ...

    // Atomic status update with conflict resolution
    _, 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
}
```

`operator.ReconcileAction` values: `ReconcileActionCreated`, `ReconcileActionUpdated`, `ReconcileActionDeleted`, `ReconcileActionResynced`.

To requeue a resource after a delay (e.g. for polling an external system), set `RequeueAfter` on the result:

```go
return operator.ReconcileResult{RequeueAfter: 10 * time.Second}, nil
```

## Status Updates with `resource.UpdateObject`

Always use `resource.UpdateObject` for status updates — it handles conflicts by fetching the latest version before applying the update function, avoiding `409 Conflict` errors common 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 how the informer watches resources 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)
                                                 // true  = use reconciler directly, no finalizer management
    },
},
```

`UsePlain: false` (default) wraps your reconciler in the `OpinionatedReconciler`, which manages finalizers automatically to ensure clean deletion.

## Watcher — Alternative to Reconciler

A `Watcher` receives distinct `Add`, `Update`, and `Delete` callbacks instead of a unified reconcile loop:

```go
type MyKindWatcher struct {
    client resource.Client
}

func (w *MyKindWatcher) Add(ctx context.Context, obj resource.Object) error {
    typed := obj.(*v1alpha1.MyKind)
    // handle create
    return nil
}

func (w *MyKindWatcher) Update(ctx context.Context, obj, old resource.Object) error {
    typed := obj.(*v1alpha1.MyKind)
    // handle update
    return nil
}

func (w *MyKindWatcher) Delete(ctx context.Context, obj resource.Object) error {
    // handle delete
    return nil
}

func (w *MyKindWatcher) Sync(ctx context.Context, obj resource.Object) error {
    // called on resync; handle like Add if needed
    return nil
}
```

Register with `Watcher` instead of `Reconciler` in `AppManagedKind`. Reconcilers are the preferred pattern; the default scaffolding still uses watchers.

## UnmanagedKinds — Watching Related Resources

To watch a kind your app doesn't own (e.g. a ConfigMap or a kind from another app), use `UnmanagedKinds` in `AppConfig`:

```go
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
        },
    },
},
```

## Registration in app.go

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

    client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).
        ClientFor(mykindv1alpha2.MyKindKind())
    if err != nil {
        return nil, fmt.Errorf("creating client: %w", err)
    }

    a, err := simple.NewApp(simple.AppConfig{
        Name:       "my-app",
        KubeConfig: cfg.KubeConfig,
        ManagedKinds: []simple.AppManagedKind{
            {
                Kind:       mykindv1alpha1.MyKindKind(),
                Validator:  NewValidator(),
                Mutator:    NewMutator(),
            },
            {
                // Attach reconciler to latest version only
                Kind:       mykindv1alpha2.MyKindKind(),
                Reconciler: NewMyKindReconciler(client),
                Validator:  NewValidator(),
                Mutator:    NewMutator(),
            },
        },
    })
    if err != nil {
      return nil, fmt.Errorf("error creating app: %w", err)
    }
    if err = a.ValidateManifest(cfg.ManifestData); err != nil {
        return nil, fmt.Errorf("app manifest validation failed: %w", err)
    }
    return a, nil
}
```

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

Files in this skill

  • SKILL.md7.8 KB
  • references/informer-configuration.md12 KB
  • references/reconciler-patterns.md9.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.