Repository: kubernetes-sigs/kro

Issue:
Support for drift-detection / re-reconciliation?
### Feature Description

**Problem Statement**:

KRO will create "child" resources, and in theory users can change (or delete) those resources.  What should KRO do in this case?

IIUC currently there is a configurable re-reconciliation interval (10 hours?) so KRO will fix/recreate any drift/deletion every 10 hours.

**Proposed Solution**:

We can watch the child objects, and then we can more rapidly respond to any drift.  (Note: we might want to allow users to _not_ correct drift, though I'm not sure whether people want this)

The solution would have to be careful both not to create "too many" watches, and to not get confused by other controllers acting on the same objects.  For example, if the status of an object changes, we should not re-reconcile.  We need to define whether we want to manage all spec fields or just the ones that user specifies (and be mindful of e.g. Deployment spec.replicas).   

If we are using watch, we should also use that for more rapid updating of the status of our "child" resources.  So this should improve performance vs polling (are we polling today?)

**Alternatives Considered**:

We could continue to use polling, or fall back to polling.  Watch is an optimization over polling.  I think we should focus this issue on the desired behaviour when "child" resources do change / are deleted, rather than how we achieve that.  (Or at least, I think they are separable decisions)

**Additional Context**:

I'm working on [applyset](https://github.com/kubernetes/enhancements/blob/master/keps/sig-cli/3659-kubectl-apply-prune/README.md) in kubectl which may have some overlap.

* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue
* If you are interested in working on this feature, please leave a comment


Discussion:
@allamand: For my own testing, it seems that the drift-detection is already in place. I tested it by updating resource group and then watching kro update the desired state. sometimes to "accelerate" i need to delete kro pods so that it directly try to reconcile, wonder how long it would wait without the reboot

It seems to try to detect some drift (deltas) already : 
```
kro-7556b99cd8-mh5xg kro 2025-02-21T13:07:29.884Z       DEBUG   controller.eksclusters  No deltas found for resource    {"namespace": "cluster1", "name": "cluster1", "resourceID": "argocdSecret"}
```

not sure for what attributes changes it consider it to be a drift (labels, annotations, specs ?)@maxstepanov: Having the ability to prune stale resources would be awesome! Currently they become orphans it seems@philipsabri: > IIUC currently there is a configurable re-reconciliation interval (10 hours?) so KRO will fix/recreate any drift/deletion every 10 hours.

Not sure if I understand the current implementation. Been playing around with this resync value (that is defaulted to 10 hours), even lowering it won't resync anything [due to this code skipping it](https://github.com/kro-run/kro/blob/bf29b582e57bc4da56305c5caac650b1c15e5e91/pkg/dynamiccontroller/dynamic_controller.go#L375-L381).

Would very much be interested in support for drift detection and re-reconciliation!@n3wscott: I think drift is a misnomer. Drift happens when there are two sources of truth, aka no source of truth. If a change happens on a managed child resource, it should be reconciled back to the state it has been templated to be in. the RGD Instance should be the only source of truth and downstream changes to that spec should be detected and immediately reverted.@github-actions: This issue has been marked **stale** due to 30 days of inactivity. Maintainers use this label for hygiene.  Comment or add a relevant label (keepalive) to keep it from becoming stale.@tjamet: I agree with @n3wscott 
Reconciler should be re-triggered when there is a change in a managed resource. This would bring KRO even closer to the Kubernetes design pattern

taking a look at the dynamic controller, I think this could be relatively easily managed by adding a primitive similar to controller-runtime's [`Owns`](https://github.com/kubernetes-sigs/controller-runtime/blob/b8f11375258f85190ec46d20726d96c2e74d95a1/pkg/builder/controller.go#L123).
It would indeed need to be adapted, but I envision being able to sligntly update the Register signature


```diff
- func (dc *DynamicController) Register(ctx context.Context, gvr schema.GroupVersionResource, handler Handler) error {
+ type Mapper interface {
+    ManagedGVR() schema.GroupVersionResource // returns the GVK of the resource in the DAG
+    Map(object unstructured.Unstructured) NamespacedName
+ }
+ func (dc *DynamicController) Register(ctx context.Context, gvr schema.GroupVersionResource, handler Handler, mappers ...Mapper) error {
```

In Register, it should `startInformer` for all the RGD and managed objects
In Deregister, it should only `stopInformer` if there are no handler *and* no *mapper* for the resource
In `enqueueObject`, it would walk through all the stored required mappers to trigger the relevant reconciliations

I believe having this would also be of great help to manage the `externalRef ` proposed in #679

in the dynamiccontroller package, there should also be a function like this pseudo-code

```
func Owns(object unstructured.Unstructured) Mapper {
    return mapper{
        gvr : resolveGVR(object),
        map: getOwnerFromUnstructuredObject,
    }
}
```

@matthchr: This has been completed based on https://github.com/kubernetes-sigs/kro/pull/611

Solution patch:
diff --git a/cmd/controller/main.go b/cmd/controller/main.go
index d595a00..ae78e6f 100644
--- a/cmd/controller/main.go
+++ b/cmd/controller/main.go
@@ -116,7 +116,7 @@ func main() {
 
 	// reconciler parameters
 	flag.IntVar(&resyncPeriod, "dynamic-controller-default-resync-period", 36000,
-		"interval at which the controller will re list resources even with no changes, in seconds")
+		"interval at which the controller will re list resources even with no changes, in seconds.")
 	flag.IntVar(&queueMaxRetries, "dynamic-controller-default-queue-max-retries", 20,
 		"maximum number of retries for an item in the queue will be retried before being dropped")
 	// log level flags
@@ -186,7 +186,7 @@ func main() {
 		MaxRetryDelay:   maxRetryDelay,
 		RateLimit:       rateLimit,
 		BurstLimit:      burstLimit,
-	}, set.Dynamic())
+	}, set.Metadata(), set.RESTMapper())
 
 	resourceGraphDefinitionGraphBuilder, err := graph.NewBuilder(
 		restConfig, set.HTTPClient(),
diff --git a/cmd/kro/commands/generate/instance.go b/cmd/kro/commands/generate/instance.go
index 0b1de29..18ad809 100644
--- a/cmd/kro/commands/generate/instance.go
+++ b/cmd/kro/commands/generate/instance.go
@@ -18,7 +18,9 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/kro-run/kro/cmd/kro/emulator"
 	"github.com/kubernetes-sigs/kro/api/v1alpha1"
+	"github.com/kubernetes-sigs/kro/pkg/graph/schema"
 	"github.com/spf13/cobra"
 	"sigs.k8s.io/yaml"
 )
@@ -57,8 +59,21 @@ func generateInstance(rgd *v1alpha1.ResourceGraphDefinition) error {
 	if err != nil {
 		return fmt.Errorf("failed to create resource graph definition: %w", err)
 	}
+	em := emulator.NewEmulator()
+
+	crd := rgdGraph.Instance.GetCRD()
+	gvk := rgdGraph.Instance.GetCRD().GroupVersionKind()
+
+	s, err := schema.ConvertJSONSchemaPropsToSpecSchema(crd.Spec.Versions[0].Schema.OpenAPIV3Schema)
+	if err != nil {
+		return fmt.Errorf("failed to convert JSONSchemaProps to SpecSchema: %w", err)
+	}
+
+	emulatedObj, err := em.GenerateDummyCR(gvk, s)
+	if err != nil {
+		return fmt.Errorf("failed to generate dummy CR: %w", err)
+	}
 
-	emulatedObj := rgdGraph.Instance.GetEmulatedObject()
 	emulatedObj.SetAnnotations(map[string]string{"kro.run/version": "dev"})
 
 	delete(emulatedObj.Object, "status")
diff --git a/pkg/graph/emulator/emulator.go b/cmd/kro/emulator/emulator.go
similarity index 100%
rename from pkg/graph/emulator/emulator.go
rename to cmd/kro/emulator/emulator.go
diff --git a/cmd/kro/go.mod b/cmd/kro/go.mod
new file mode 100644
index 0000000..a2d1f81
--- /dev/null
+++ b/cmd/kro/go.mod
@@ -0,0 +1,79 @@
+module github.com/kro-run/kro/cmd/kro
+
+go 1.24.0
+
+require (
+	github.com/go-echarts/go-echarts/v2 v2.6.5
+	github.com/kubernetes-sigs/kro v0.5.0
+	github.com/spf13/cobra v1.10.1
+	github.com/stretchr/testify v1.11.1
+	gopkg.in/yaml.v2 v2.4.0
+	k8s.io/apimachinery v0.34.1
+	k8s.io/client-go v0.34.1
+	k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
+	sigs.k8s.io/yaml v1.6.0
+)
+
+require (
+	cel.dev/expr v0.19.1 // indirect
+	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/gobuffalo/flect v1.0.2 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/google/cel-go v0.24.1 // indirect
+	github.com/google/gnostic-models v0.7.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.19.1 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/prometheus/common v0.55.0 // indirect
+	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/spf13/pflag v1.0.9 // indirect
+	github.com/stoewer/go-strcase v1.2.0 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
+	golang.org/x/net v0.43.0 // indirect
+	golang.org/x/oauth2 v0.28.0 // indirect
+	golang.org/x/sync v0.16.0 // indirect
+	golang.org/x/sys v0.35.0 // indirect
+	golang.org/x/term v0.34.0 // indirect
+	golang.org/x/text v0.28.0 // indirect
+	golang.org/x/time v0.9.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
+	google.golang.org/protobuf v1.36.5 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/api v0.34.1 // indirect
+	k8s.io/apiextensions-apiserver v0.31.0 // indirect
+	k8s.io/apiserver v0.31.0 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
+	sigs.k8s.io/controller-runtime v0.19.0 // indirect
+	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/release-utils v0.11.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+)
diff --git a/cmd/kro/go.sum b/cmd/kro/go.sum
new file mode 100644
index 0000000..7d22d6b
--- /dev/null
+++ b/cmd/kro/go.sum
@@ -0,0 +1,216 @@
+cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
+cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
+github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
+github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-echarts/go-echarts/v2 v2.6.5 h1:f6h2Yt8j8G2profitrlhZvE7mJe6iAI7vYmG2Qqil+M=
+github.com/go-echarts/go-echarts/v2 v2.6.5/go.mod h1:Z+spPygZRIEyqod69r0WMnkN5RV3MwhYDtw601w3G8w=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA=
+github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
+github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kubernetes-sigs/kro v0.5.0 h1:R6K8QtM1l48JLv/grUEEy1ZfchQdasnlfkYmIjn5Zxo=
+github.com/kubernetes-sigs/kro v0.5.0/go.mod h1:nYTAocjXDWxV47FR7HemRZwPF+gRPOvLt6CLuXlPq8I=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
+golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
+google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
+k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
+k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=
+k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
+k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
+k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
+k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
+k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
+k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
+sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/release-utils v0.11.0 h1:FUVSw2dO67M7mfcQx9AITEGnTHoBOdJNbbQ3FT3o8mA=
+sigs.k8s.io/release-utils v0.11.0/go.mod h1:wAlXz8xruzvqZUsorI64dZ3lbkiDnYSlI4IYC6l2yEA=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/go.mod b/go.mod
index f378c77..6d4f17b 100644
--- a/go.mod
+++ b/go.mod
@@ -3,63 +3,56 @@ module github.com/kubernetes-sigs/kro
 go 1.24.0
 
 require (
-	github.com/go-echarts/go-echarts/v2 v2.6.1
 	github.com/go-logr/logr v1.4.2
 	github.com/gobuffalo/flect v1.0.2
-	github.com/google/cel-go v0.24.1
-	github.com/google/go-cmp v0.6.0
+	github.com/google/cel-go v0.26.0
+	github.com/google/go-cmp v0.7.0
 	github.com/hashicorp/golang-lru/v2 v2.0.7
-	github.com/onsi/ginkgo/v2 v2.20.0
-	github.com/onsi/gomega v1.34.1
-	github.com/prometheus/client_golang v1.19.1
-	github.com/spf13/cobra v1.8.1
+	github.com/onsi/ginkgo/v2 v2.21.0
+	github.com/onsi/gomega v1.35.1
+	github.com/prometheus/client_golang v1.22.0
 	github.com/stretchr/testify v1.10.0
-	go.uber.org/zap v1.26.0
+	go.uber.org/zap v1.27.0
 	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
 	golang.org/x/sync v0.12.0
-	golang.org/x/time v0.3.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7
-	gopkg.in/yaml.v2 v2.4.0
-	k8s.io/api v0.31.0
+	golang.org/x/time v0.9.0
+	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb
+	k8s.io/api v0.34.1
 	k8s.io/apiextensions-apiserver v0.31.0
-	k8s.io/apimachinery v0.31.0
-	k8s.io/apiserver v0.31.0
-	k8s.io/client-go v0.31.0
-	k8s.io/kube-openapi v0.0.0-20240816214639-573285566f34
-	k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
+	k8s.io/apimachinery v0.34.1
+	k8s.io/apiserver v0.34.1
+	k8s.io/client-go v0.34.1
+	k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
+	k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
 	sigs.k8s.io/controller-runtime v0.19.0
 	sigs.k8s.io/release-utils v0.11.0
-	sigs.k8s.io/yaml v1.6.0
 )
 
 require (
-	cel.dev/expr v0.19.1 // indirect
+	cel.dev/expr v0.24.0 // indirect
 	github.com/B1NARY-GR0UP/nwa v0.5.2 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/blang/semver/v4 v4.0.0 // indirect
 	github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
 	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
-	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
 	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/golang/protobuf v1.5.4 // indirect
-	github.com/google/gnostic-models v0.6.8 // indirect
-	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
+	github.com/google/gnostic-models v0.7.0 // indirect
+	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/imdario/mergo v0.3.6 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -67,24 +60,26 @@ require (
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.55.0 // indirect
+	github.com/prometheus/common v0.62.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/spf13/afero v1.9.5 // indirect
 	github.com/spf13/cast v1.5.1 // indirect
+	github.com/spf13/cobra v1.9.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/spf13/viper v1.16.0 // indirect
-	github.com/stoewer/go-strcase v1.2.0 // indirect
+	github.com/stoewer/go-strcase v1.3.0 // indirect
 	github.com/subosito/gotenv v1.4.2 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/net v0.38.0 // indirect
 	golang.org/x/oauth2 v0.28.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
@@ -92,15 +87,17 @@ require (
 	golang.org/x/text v0.23.0 // indirect
 	golang.org/x/tools v0.28.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
-	google.golang.org/protobuf v1.34.2 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
+	google.golang.org/protobuf v1.36.5 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
-	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
+	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
 )
 
 tool github.com/B1NARY-GR0UP/nwa
diff --git a/go.sum b/go.sum
index 5e06850..74b2788 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
-cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
+cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -46,6 +46,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
 github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -60,13 +62,13 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
-github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -79,12 +81,10 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
 github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
-github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
-github.com/go-echarts/go-echarts/v2 v2.6.1 h1:UjyovbU7sbALakMYaoFsSKimT1Sm3kHCJcJSu6U5JoU=
-github.com/go-echarts/go-echarts/v2 v2.6.1/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -108,8 +108,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -131,14 +129,12 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
-github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
-github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
-github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
+github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -148,9 +144,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -167,8 +162,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
-github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -184,8 +179,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
-github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -196,6 +189,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -204,6 +199,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -213,14 +210,15 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw=
-github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
-github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
-github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
 github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
 github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -229,36 +227,38 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
 github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
 github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
 github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
 github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
-github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
-github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -284,16 +284,20 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
-go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
-go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -446,8 +450,8 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -567,10 +571,10 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
-google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
+google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -597,8 +601,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -611,9 +615,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -624,32 +625,36 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
-k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
+k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
+k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
 k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=
 k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
-k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
-k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
-k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
-k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
-k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
-k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
+k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
+k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
+k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
+k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
+k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
+k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
+k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20240816214639-573285566f34 h1:/amS69DLm09mtbFtN3+LyygSFohnYGMseF8iv+2zulg=
-k8s.io/kube-openapi v0.0.0-20240816214639-573285566f34/go.mod h1:G0W3eI9gG219NHRq3h5uQaRBl4pj4ZpwzRP5ti8y770=
-k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
-k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
 sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
 sigs.k8s.io/release-utils v0.11.0 h1:FUVSw2dO67M7mfcQx9AITEGnTHoBOdJNbbQ3FT3o8mA=
 sigs.k8s.io/release-utils v0.11.0/go.mod h1:wAlXz8xruzvqZUsorI64dZ3lbkiDnYSlI4IYC6l2yEA=
-sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
-sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
 sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
 sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/pkg/cel/conversions.go b/pkg/cel/conversions.go
index 0a92f19..d80c99d 100644
--- a/pkg/cel/conversions.go
+++ b/pkg/cel/conversions.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"reflect"
 
+	"github.com/google/cel-go/cel"
 	"github.com/google/cel-go/common/types"
 	"github.com/google/cel-go/common/types/ref"
 )
@@ -63,3 +64,36 @@ func GoNativeType(v ref.Val) (interface{}, error) {
 func IsBoolType(v ref.Val) bool {
 	return v.Type() == types.BoolType
 }
+
+// WouldMatchIfUnwrapped checks if outputType would be assignable to expectedType
+// if we unwrapped the optional wrapper from outputType.
+// This detects the case where outputType is optional_type(T) and expectedType is T.
+func WouldMatchIfUnwrapped(outputType, expectedType *cel.Type) bool {
+	if outputType == nil || expectedType == nil {
+		return false
+	}
+
+	// If output is already assignable to expected, no mismatch
+	if expectedType.IsAssignableType(outputType) {
+		return false
+	}
+
+	// Check if wrapping expected in optional makes it match output
+	// If yes, then output is an optional version of expected
+	optionalExpected := cel.OptionalType(expectedType)
+	return optionalExpected.IsAssignableType(outputType)
+}
+
+// IsBoolOrOptionalBool checks if a CEL type is bool or optional_type(bool).
+// This is useful for validating condition expressions that must return boolean values.
+func IsBoolOrOptionalBool(t *cel.Type) bool {
+	// Check if it's a direct bool type
+	// Note: A.IsAssignableType(B) means "A accepts B", so we check if bool accepts t
+	if cel.BoolType.IsAssignableType(t) {
+		return true
+	}
+
+	// Check if it's optional_type(bool)
+	optionalBool := cel.OptionalType(cel.BoolType)
+	return optionalBool.IsAssignableType(t)
+}
diff --git a/pkg/cel/environment.go b/pkg/cel/environment.go
index edd56aa..151542d 100644
--- a/pkg/cel/environment.go
+++ b/pkg/cel/environment.go
@@ -15,8 +15,14 @@
 package cel
 
 import (
+	"maps"
+
 	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common/types"
 	"github.com/google/cel-go/ext"
+	apiservercel "k8s.io/apiserver/pkg/cel"
+	"k8s.io/apiserver/pkg/cel/openapi"
+	"k8s.io/kube-openapi/pkg/validation/spec"
 
 	"github.com/kubernetes-sigs/kro/pkg/cel/library"
 )
@@ -28,9 +34,19 @@ type EnvOption func(*envOptions)
 type envOptions struct {
 	// resourceIDs will be converted to CEL variable declarations
 	// of type 'any'.
-	//
-	// TODO(a-hilaly): Add support for custom types.
 	resourceIDs []string
+	// typedResources maps resource names to their OpenAPI schemas.
+	// These will be converted to typed CEL variables with field-level
+	// type checking enabled.
+	//
+	// Note that there is not a 1:1 mapping between CEL types and OpenAPI
+	// schemas. This is best effort conversion to enable type checking
+	// for field access in CEL expressions.
+	//
+	// Native CEL types (like int, bool, list, map) will be used where
+	// possible. OpenAPI's AnyOf, OneOf, and VendorExtensions features like
+	// x-kubernetes-int-or-string will fall back to dyn or any type.
+	typedResources map[string]*spec.Schema
 	// customDeclarations will be added to the CEL environment.
 	customDeclarations []cel.EnvOption
 }
@@ -49,6 +65,18 @@ func WithCustomDeclarations(declarations []cel.EnvOption) EnvOption {
 	}
 }
 
+// WithTypedResources adds typed resource declarations to the CEL environment.
+// This enables compile time type checking for field access in CEL expressions.
+func WithTypedResources(schemas map[string]*spec.Schema) EnvOption {
+	return func(opts *envOptions) {
+		if opts.typedResources == nil {
+			opts.typedResources = schemas
+		} else {
+			maps.Copy(opts.typedResources, schemas)
+		}
+	}
+}
+
 // DefaultEnvironment returns the default CEL environment.
 func DefaultEnvironment(options ...EnvOption) (*cel.Env, error) {
 	declarations := []cel.EnvOption{
@@ -66,9 +94,86 @@ func DefaultEnvironment(options ...EnvOption) (*cel.Env, error) {
 
 	declarations = append(declarations, opts.customDeclarations...)
 
+	if len(opts.typedResources) > 0 {
+		// We need both a TypeProvider (for field resolution) and variable declarations.
+		// To avoid conflicts, we use different names for types vs variables:
+		//  - Types are registered with "__type_<name>" prefix (e.g "__type_schema")
+		//  - Variables use the original names (e.g "pod", "schema"...)
+
+		declTypes := make([]*apiservercel.DeclType, 0, len(opts.typedResources))
+
+		for name, schema := range opts.typedResources {
+			declType := openapi.SchemaDeclType(schema, false)
+			if declType != nil {
+				typeName := "__type_" + name
+				declType = declType.MaybeAssignTypeName(typeName)
+
+				// add type declaration
+				declTypes = append(declTypes, declType)
+
+				celType := declType.CelType()
+
+				// Add variable declaration
+				declarations = append(declarations, cel.Variable(name, celType))
+			}
+		}
+
+		if len(declTypes) > 0 {
+			baseProvider := apiservercel.NewDeclTypeProvider(declTypes...)
+
+			registry := types.NewEmptyRegistry()
+			wrappedProvider, err := baseProvider.WithTypeProvider(registry)
+			if err != nil {
+				return nil, err
+			}
+
+			declarations = append(declarations, cel.CustomTypeProvider(wrappedProvider))
+		}
+	}
+
 	for _, name := range opts.resourceIDs {
 		declarations = append(declarations, cel.Variable(name, cel.AnyType))
 	}
 
 	return cel.NewEnv(declarations...)
 }
+
+// TypedEnvironment creates a CEL environment with type checking enabled.
+//
+// This should be used during RGD build time (pkg/graph.Builder) to validate
+// CEL expressions against OpenAPI schemas.
+func TypedEnvironment(schemas map[string]*spec.Schema) (*cel.Env, error) {
+	return DefaultEnvironment(WithTypedResources(schemas))
+}
+
+// UntypedEnvironment creates a CEL environment without type declarations.
+//
+// This is theoretically cheaper to use as there are no Schema conversions
+// required. NOTE(a-hilaly): maybe use this for runtime? undecided.
+func UntypedEnvironment(resourceIDs []string) (*cel.Env, error) {
+	return DefaultEnvironment(WithResourceIDs(resourceIDs))
+}
+
+// CreateDeclTypeProvider creates a DeclTypeProvider from OpenAPI schemas.
+// This is used for deep introspection of type structures when generating schemas.
+// The provider maps CEL type names to their full DeclType definitions with all fields.
+func CreateDeclTypeProvider(schemas map[string]*spec.Schema) *apiservercel.DeclTypeProvider {
+	if len(schemas) == 0 {
+		return nil
+	}
+
+	declTypes := make([]*apiservercel.DeclType, 0, len(schemas))
+	for name, schema := range schemas {
+		declType := openapi.SchemaDeclType(schema, false)
+		if declType != nil {
+			declType = declType.MaybeAssignTypeName(name)
+			declTypes = append(declTypes, declType)
+		}
+	}
+
+	if len(declTypes) == 0 {
+		return nil
+	}
+
+	return apiservercel.NewDeclTypeProvider(declTypes...)
+}
diff --git a/pkg/client/set.go b/pkg/client/set.go
index 2d5aaf4..26e1d47 100644
--- a/pkg/client/set.go
+++ b/pkg/client/set.go
@@ -22,7 +22,9 @@ import (
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/metadata"
 	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 	ctrlrtconfig "sigs.k8s.io/controller-runtime/pkg/client/config"
 	"sigs.k8s.io/release-utils/version"
 )
@@ -58,6 +60,7 @@ type Set struct {
 	config          *rest.Config
 	kubernetes      *kubernetes.Clientset
 	dynamic         *dynamic.DynamicClient
+	metadata        metadata.Interface
 	apiExtensionsV1 *apiextensionsv1.ApiextensionsV1Client
 	// restMapper is a REST mapper for the Kubernetes API server
 	restMapper meta.RESTMapper
@@ -124,6 +127,11 @@ func (c *Set) init() error {
 		return err
 	}
 
+	c.metadata, err = metadata.NewForConfigAndClient(c.config, c.httpClient)
+	if err != nil {
+		return err
+	}
+
 	c.dynamic, err = dynamic.NewForConfigAndClient(c.config, c.httpClient)
 	if err != nil {
 		return err
@@ -134,6 +142,11 @@ func (c *Set) init() error {
 		return err
 	}
 
+	c.restMapper, err = apiutil.NewDynamicRESTMapper(c.config, c.httpClient)
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -146,6 +159,11 @@ func (c *Set) Kubernetes() kubernetes.Interface {
 	return c.kubernetes
 }
 
+// Metadata returns the metadata client
+func (c *Set) Metadata() metadata.Interface {
+	return c.metadata
+}
+
 // Dynamic returns the dynamic client
 func (c *Set) Dynamic() dynamic.Interface {
 	return c.dynamic
diff --git a/pkg/controller/resourcegraphdefinition/controller.go b/pkg/controller/resourcegraphdefinition/controller.go
index 33767d5..a6ed923 100644
--- a/pkg/controller/resourcegraphdefinition/controller.go
+++ b/pkg/controller/resourcegraphdefinition/controller.go
@@ -155,7 +155,10 @@ func (r *ResourceGraphDefinitionReconciler) findRGDsForCRD(ctx context.Context,
 	}
 }
 
-func (r *ResourceGraphDefinitionReconciler) Reconcile(ctx context.Context, o *v1alpha1.ResourceGraphDefinition) (ctrl.Result, error) {
+func (r *ResourceGraphDefinitionReconciler) Reconcile(
+	ctx context.Context,
+	o *v1alpha1.ResourceGraphDefinition,
+) (ctrl.Result, error) {
 	if !o.DeletionTimestamp.IsZero() {
 		if err := r.cleanupResourceGraphDefinition(ctx, o); err != nil {
 			return ctrl.Result{}, err
diff --git a/pkg/controller/resourcegraphdefinition/controller_reconcile.go b/pkg/controller/resourcegraphdefinition/controller_reconcile.go
index c174e66..3072144 100644
--- a/pkg/controller/resourcegraphdefinition/controller_reconcile.go
+++ b/pkg/controller/resourcegraphdefinition/controller_reconcile.go
@@ -17,6 +17,8 @@ package resourcegraphdefinition
 import (
 	"context"
 	"fmt"
+	"maps"
+	"slices"
 	"time"
 
 	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -25,7 +27,6 @@ import (
 
 	"github.com/kubernetes-sigs/kro/api/v1alpha1"
 	instancectrl "github.com/kubernetes-sigs/kro/pkg/controller/instance"
-	"github.com/kubernetes-sigs/kro/pkg/dynamiccontroller"
 	"github.com/kubernetes-sigs/kro/pkg/graph"
 	"github.com/kubernetes-sigs/kro/pkg/metadata"
 )
@@ -72,15 +73,10 @@ func (r *ResourceGraphDefinitionReconciler) reconcileResourceGraphDefinition(
 		mark.KindReady(crd.Status.AcceptedNames.Kind)
 	}
 
-	// Setup and start microcontroller
-	gvr := processedRGD.Instance.GetGroupVersionResource()
-	controller := r.setupMicroController(gvr, processedRGD, graphExecLabeler)
-
-	log.V(1).Info("reconciling resource graph definition micro controller")
 	// TODO: the context that is passed here is tied to the reconciliation of the rgd, we might need to make
 	// a new context with our own cancel function here to allow us to cleanly term the dynamic controller
 	// rather than have it ignore this context and use the background context.
-	if err := r.reconcileResourceGraphDefinitionMicroController(ctx, &gvr, controller.Reconcile); err != nil {
+	if err := r.reconcileResourceGraphDefinitionMicroController(ctx, processedRGD, graphExecLabeler); err != nil {
 		mark.ControllerFailedToStart(err.Error())
 		return processedRGD.TopologicalOrder, resourcesInfo, err
 	}
@@ -89,6 +85,14 @@ func (r *ResourceGraphDefinitionReconciler) reconcileResourceGraphDefinition(
 	return processedRGD.TopologicalOrder, resourcesInfo, nil
 }
 
+func (r *ResourceGraphDefinitionReconciler) getResourceGVRsToWatchForRGD(processedRGD *graph.Graph) []schema.GroupVersionResource {
+	resourceHandlers := make(map[schema.GroupVersionResource]struct{}, len(processedRGD.Resources))
+	for _, resource := range processedRGD.Resources {
+		resourceHandlers[resource.GetGroupVersionResource()] = struct{}{}
+	}
+	return slices.Collect(maps.Keys(resourceHandlers))
+}
+
 // setupLabeler creates and merges the required labelers for the resource graph definition
 func (r *ResourceGraphDefinitionReconciler) setupLabeler(rgd *v1alpha1.ResourceGraphDefinition) (metadata.Labeler, error) {
 	rgLabeler := metadata.NewResourceGraphDefinitionLabeler(rgd)
@@ -97,10 +101,10 @@ func (r *ResourceGraphDefinitionReconciler) setupLabeler(rgd *v1alpha1.ResourceG
 
 // setupMicroController creates a new controller instance with the required configuration
 func (r *ResourceGraphDefinitionReconciler) setupMicroController(
-	gvr schema.GroupVersionResource,
 	processedRGD *graph.Graph,
 	labeler metadata.Labeler,
 ) *instancectrl.Controller {
+	gvr := processedRGD.Instance.GetGroupVersionResource()
 	instanceLogger := r.instanceLogger.WithName(fmt.Sprintf("%s-controller", gvr.Resource)).WithValues(
 		"controller", gvr.Resource,
 		"controllerGroup", processedRGD.Instance.GetCRD().Spec.Group,
@@ -162,8 +166,22 @@ func (r *ResourceGraphDefinitionReconciler) reconcileResourceGraphDefinitionCRD(
 }
 
 // reconcileResourceGraphDefinitionMicroController starts the microcontroller for handling the resources
-func (r *ResourceGraphDefinitionReconciler) reconcileResourceGraphDefinitionMicroController(ctx context.Context, gvr *schema.GroupVersionResource, handler dynamiccontroller.Handler) error {
-	err := r.dynamicController.Register(ctx, *gvr, handler)
+func (r *ResourceGraphDefinitionReconciler) reconcileResourceGraphDefinitionMicroController(
+	ctx context.Context,
+	processedRGD *graph.Graph,
+	graphExecLabeler metadata.Labeler,
+) error {
+	// If we want to react to changes to resources, we need to watch for them
+	// and trigger reconciliations of the instances whenever these resources change.
+	resourceGVRsToWatch := r.getResourceGVRsToWatchForRGD(processedRGD)
+
+	// Setup and start microcontroller
+	controller := r.setupMicroController(processedRGD, graphExecLabeler)
+
+	ctrl.LoggerFrom(ctx).V(1).Info("reconciling resource graph definition micro controller")
+	gvr := processedRGD.Instance.GetGroupVersionResource()
+
+	err := r.dynamicController.Register(ctx, gvr, controller.Reconcile, resourceGVRsToWatch...)
 	if err != nil {
 		return newMicroControllerError(err)
 	}
diff --git a/pkg/dynamiccontroller/dynamic_controller.go b/pkg/dynamiccontroller/dynamic_controller.go
index 52b32e3..58b2588 100644
--- a/pkg/dynamiccontroller/dynamic_controller.go
+++ b/pkg/dynamiccontroller/dynamic_controller.go
@@ -66,22 +66,31 @@ import (
 	"github.com/go-logr/logr"
 	"golang.org/x/time/rate"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
-	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/dynamic/dynamicinformer"
+	k8smetadata "k8s.io/client-go/metadata"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/util/workqueue"
 	ctrl "sigs.k8s.io/controller-runtime"
 
+	"github.com/kubernetes-sigs/kro/pkg/dynamiccontroller/internal"
 	"github.com/kubernetes-sigs/kro/pkg/metadata"
 	"github.com/kubernetes-sigs/kro/pkg/requeue"
 )
 
+const (
+	// eventTypeAdd is emitted to the queue when a new object was created
+	eventTypeAdd = "add"
+	// eventTypeUpdate is emitted to the queue when an existing object was updated
+	eventTypeUpdate = "update"
+	// eventTypeDelete is emitted to the queue when an existing object was deleted
+	eventTypeDelete = "delete"
+)
+
 // Config holds the configuration for DynamicController
 type Config struct {
 	// Workers specifies the number of workers processing items from the queue
@@ -103,6 +112,27 @@ type Config struct {
 	RateLimit int
 	// BurstLimit is the maximum number of events in a burst
 	BurstLimit int
+	// QueueShutdownTimeout is the maximum time to wait for the queue to drain before shutting down.
+	QueueShutdownTimeout time.Duration
+}
+
+// Handler is used to actually perform the reconciliation logic for an instance GVR and will operate
+// on a single instance of the resource received from the queue
+type Handler func(ctx context.Context, req ctrl.Request) error
+
+// ObjectIdentifiers holds the key and GVR of the object to reconcile.
+type ObjectIdentifiers struct {
+	types.NamespacedName
+	GVR schema.GroupVersionResource
+}
+
+// registration tracks one parent GVR registration and its child handler IDs.
+// Each parent may own one "parent handler" and multiple "child handlers".
+type registration struct {
+	parentGVR schema.GroupVersionResource
+
+	parentHandlerID string
+	childHandlerIDs map[schema.GroupVersionResource]string
 }
 
 // DynamicController (DC) is a single controller capable of managing multiple different
@@ -116,149 +146,111 @@ type Config struct {
 // dynamic resource management system. Its primary purpose is to create and manage
 // "micro" controllers for custom resources defined by users at runtime (via the
 // ResourceGraphDefinition CRs).
+//
+// DynamicController manages all handlers and informers.
+// It uses two levels of locking:
+//
+//  1. dc.mu protects *global maps* (watches and registrations).
+//     This ensures consistency when adding/removing GVRs or
+//     attaching/detaching children.
+//
+//  2. Each perGVRWatch has its own w.mu to protect *per-informer state*
+//     (handler map). This allows concurrent updates to
+//     different GVRs without blocking each other.
 type DynamicController struct {
+	// Parent run context, inherited by all informer stop contexts.
+	// It is set by Start and meant to be used to register the controller context with a global handler
+	// such as the controller-runtime manager.
+	ctx context.Context
+
+	// mu is a global mutex protecting watches and registrations.
+	// Required because StartServingGVK and StopServiceGVK may run concurrently,
+	// and because Run or gracefulShutdown may also traverse these maps.
+	mu sync.Mutex
+
+	// config is the controller configuration used to steer detailed enqueue behavior when events
+	// are received by the dynamic controllers watches and need to be propagated to the handlers.
 	config Config
 
-	// kubeClient is the dynamic client used to create the informers
-	kubeClient dynamic.Interface
-	// informers is a safe map of GVR to informers. Each informer is responsible
-	// for watching a specific GVR.
-	informers sync.Map
-
-	// handlers is a safe map of GVR to workflow operators. Each
-	// handler is responsible for managing a specific GVR.
-	handlers sync.Map
-
-	// queue is the workqueue used to process items
-	queue workqueue.TypedRateLimitingInterface[ObjectIdentifiers]
-
 	log logr.Logger
-}
-
-type Handler func(ctx context.Context, req ctrl.Request) error
 
-type informerWrapper struct {
-	informer dynamicinformer.DynamicSharedInformerFactory
-	shutdown func()
+	// client is the Kubernetes client used to create informers and list resources.
+	// DynamicController only operates on metadata information, because all actual reconciliations
+	// happen in the instance handlers that are managed with Register and Deregister.
+	client k8smetadata.Interface
+	// mapper is used to translate from GVRs (REST Resources that can be watched) to GVKs to properly
+	// propagate instance events to the handlers.
+	mapper meta.RESTMapper
+
+	// Map of active informers per GVR.
+	// No matter if an instance resource or a child resource is being watched,
+	// eventually, they will be handled by an internal.LazyInformer registered here.
+	// Guarded by mu.
+	watches map[schema.GroupVersionResource]*internal.LazyInformer
+	// Map of parent registrations per GVR.
+	// A registration is created for each parent GVR that is being watched.
+	// Each registration contains a list of child GVRs that are being watched for the parent.
+	// Any event that is received for a child GVR will be propagated through a labelled reference to the parent.
+	// Guarded by mu.
+	registrations map[schema.GroupVersionResource]*registration
+
+	// handlers is a Handler collection for each parent GVR, invoked for queued objects.
+	handlers sync.Map // map[schema.GroupVersionResource]Handler (thread-safe on its own)
+	// queue is the work queue used to process items received via watches.
+	// The queue is shared between all informers and is used to propagate events to the handlers.
+	queue workqueue.TypedRateLimitingInterface[ObjectIdentifiers]
 }
 
-// NewDynamicController creates a new DynamicController instance.
+// NewDynamicController creates a new DynamicController.
 func NewDynamicController(
 	log logr.Logger,
 	config Config,
-	kubeClient dynamic.Interface) *DynamicController {
+	kubeClient k8smetadata.Interface,
+	mapper meta.RESTMapper,
+) *DynamicController {
 	logger := log.WithName("dynamic-controller")
 
-	dc := &DynamicController{
-		config:     config,
-		kubeClient: kubeClient,
+	return &DynamicController{
+		config:        config,
+		log:           logger,
+		client:        kubeClient,
+		mapper:        mapper,
+		watches:       make(map[schema.GroupVersionResource]*internal.LazyInformer),
+		registrations: make(map[schema.GroupVersionResource]*registration),
 		queue: workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.NewTypedMaxOfRateLimiter(
 			workqueue.NewTypedItemExponentialFailureRateLimiter[ObjectIdentifiers](config.MinRetryDelay, config.MaxRetryDelay),
 			&workqueue.TypedBucketRateLimiter[ObjectIdentifiers]{Limiter: rate.NewLimiter(rate.Limit(config.RateLimit), config.BurstLimit)},
 		), workqueue.TypedRateLimitingQueueConfig[ObjectIdentifiers]{Name: "dynamic-controller-queue"}),
-		log: logger,
-		// pass version and pod id from env
 	}
-
-	return dc
 }
 
-// AllInformerHaveSynced checks if all registered informers have synced, returns
-// true if they have.
-func (dc *DynamicController) AllInformerHaveSynced() bool {
-	var allSynced bool
-	var informerCount int
-
-	// Unfortunately we can't know the number of informers in advance, so we need to
-	// iterate over all of them to check if they have synced.
-
-	dc.informers.Range(func(key, value interface{}) bool {
-		informerCount++
-		// possibly panic if the value is not a SharedIndexInformer
-		informer, ok := value.(cache.SharedIndexInformer)
-		if !ok {
-			dc.log.Error(nil, "Failed to cast informer", "key", key)
-			allSynced = false
-			return false
-		}
-		if !informer.HasSynced() {
-			allSynced = false
-			return false
-		}
-		return true
-	})
-
-	if informerCount == 0 {
-		return true
+// Start starts workers and blocks until ctx.Done().
+func (dc *DynamicController) Start(ctx context.Context) error {
+	if dc.ctx != nil {
+		return fmt.Errorf("already running")
 	}
-	return allSynced
-}
 
-// WaitForInformerSync waits for all informers to sync or timeout
-func (dc *DynamicController) WaitForInformersSync(stopCh <-chan struct{}) bool {
-	dc.log.V(1).Info("Waiting for all informers to sync")
-	start := time.Now()
-	defer func() {
-		dc.log.V(1).Info("Finished waiting for informers to sync", "duration", time.Since(start))
-	}()
-
-	return cache.WaitForCacheSync(stopCh, dc.AllInformerHaveSynced)
-}
-
-// Run starts the DynamicController.
-func (dc *DynamicController) Start(ctx context.Context) error {
 	defer utilruntime.HandleCrash()
-	defer dc.queue.ShutDown()
 
 	dc.log.Info("Starting dynamic controller")
 	defer dc.log.Info("Shutting down dynamic controller")
 
-	// Wait for all informers to sync
-	if !dc.WaitForInformersSync(ctx.Done()) {
-		return fmt.Errorf("failed to sync informers")
-	}
+	dc.ctx = ctx
 
-	// Spin up workers.
-	//
-	// TODO(a-hilaly): Allow for dynamic scaling of workers.
-	var wg sync.WaitGroup
+	// Workers.
 	for i := 0; i < dc.config.Workers; i++ {
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
-			wait.UntilWithContext(ctx, dc.worker, time.Second)
-		}()
+		go wait.UntilWithContext(ctx, dc.worker, time.Second)
 	}
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-		<-ctx.Done()
-		dc.log.Info("Received shutdown signal, shutting down dynamic controller queue")
-		dc.queue.ShutDown()
-	}()
 
-	wg.Wait()
-	dc.log.Info("All workers have stopped")
-
-	// when shutting down, the context given to Start is already closed,
-	// and the expectation is that we block until the graceful shutdown is complete.
-	return dc.shutdown(context.Background())
+	<-ctx.Done()
+	return dc.gracefulShutdown()
 }
 
-// worker processes items from the queue.
 func (dc *DynamicController) worker(ctx context.Context) {
-	for {
-		select {
-		case <-ctx.Done():
-			dc.log.Info("Dynamic controller worker received shutdown signal, stopping")
-			return
-		default:
-			dc.processNextWorkItem(ctx)
-		}
+	for dc.processNextWorkItem(ctx) {
 	}
 }
 
-// processNextWorkItem processes a single item from the queue.
 func (dc *DynamicController) processNextWorkItem(ctx context.Context) bool {
 	item, shutdown := dc.queue.Get()
 	if shutdown {
@@ -266,17 +258,25 @@ func (dc *DynamicController) processNextWorkItem(ctx context.Context) bool {
 	}
 	defer dc.queue.Done(item)
 
+	// metric: queueLength
 	queueLength.Set(float64(dc.queue.Len()))
 
-	err := dc.syncFunc(ctx, item)
+	handler, ok := dc.handlers.Load(item.GVR)
+	if !ok {
+		// this can happen if the handler was removed and we still have items in flight in the queue.
+		dc.log.V(1).Info("handler for gvr no longer exists, dropping item", "item", item)
+		dc.queue.Forget(item)
+		return true
+	}
+
+	err := dc.syncFunc(ctx, item, handler.(Handler))
 	if err == nil || apierrors.IsNotFound(err) {
 		dc.queue.Forget(item)
 		return true
 	}
 
-	gvrKey := fmt.Sprintf("%s/%s/%s", item.GVR.Group, item.GVR.Version, item.GVR.Resource)
+	gvrKey := keyFromGVR(item.GVR)
 
-	// Handle requeues
 	switch typedErr := err.(type) {
 	case *requeue.NoRequeue:
 		dc.log.Error(typedErr, "Error syncing item, not requeuing", "item", item)
@@ -285,14 +285,12 @@ func (dc *DynamicController) processNextWorkItem(ctx context.Context) bool {
 	case *requeue.RequeueNeeded:
 		dc.log.V(1).Info("Requeue needed", "item", item, "error", typedErr)
 		requeueTotal.WithLabelValues(gvrKey, "requeue").Inc()
-		dc.queue.Add(item) // Add without rate limiting
+		dc.queue.Add(item)
 	case *requeue.RequeueNeededAfter:
 		dc.log.V(1).Info("Requeue needed after delay", "item", item, "error", typedErr, "delay", typedErr.Duration())
 		requeueTotal.WithLabelValues(gvrKey, "requeue_after").Inc()
 		dc.queue.AddAfter(item, typedErr.Duration())
 	default:
-		// Arriving here means we have an unexpected error, we should requeue the item
-		// with rate limiting.
 		requeueTotal.WithLabelValues(gvrKey, "rate_limited").Inc()
 		if dc.queue.NumRequeues(item) < dc.config.QueueMaxRetries {
 			dc.log.Error(err, "Error syncing item, requeuing with rate limit", "item", item)
@@ -306,10 +304,9 @@ func (dc *DynamicController) processNextWorkItem(ctx context.Context) bool {
 	return true
 }
 
-// syncFunc reconciles a single item.
-func (dc *DynamicController) syncFunc(ctx context.Context, oi ObjectIdentifiers) error {
-	gvrKey := fmt.Sprintf("%s/%s/%s", oi.GVR.Group, oi.GVR.Version, oi.GVR.Resource)
-	dc.log.V(1).Info("Syncing object", "gvr", gvrKey, "namespacedKey", oi.NamespacedKey)
+func (dc *DynamicController) syncFunc(ctx context.Context, oi ObjectIdentifiers, handler Handler) error {
+	gvrKey := keyFromGVR(oi.GVR)
+	dc.log.V(1).Info("Syncing object", "gvr", gvrKey, "key", oi.NamespacedName)
 
 	startTime := time.Now()
 	defer func() {
@@ -317,254 +314,362 @@ func (dc *DynamicController) syncFunc(ctx context.Context, oi ObjectIdentifiers)
 		reconcileDuration.WithLabelValues(gvrKey).Observe(duration.Seconds())
 		reconcileTotal.WithLabelValues(gvrKey).Inc()
 		dc.log.V(1).Info("Finished syncing object",
-			"gvr", gvrKey,
-			"namespacedKey", oi.NamespacedKey,
-			"duration", duration)
+			"gvr", gvrKey, "key", oi.NamespacedName, "duration", duration)
 	}()
 
-	genericHandler, ok := dc.handlers.Load(oi.GVR)
-	if !ok {
-		// NOTE(a-hilaly): this might mean that the GVR is not registered, or the workflow operator
-		// is not found. We should probably handle this in a better way.
-		return fmt.Errorf("no handler found for GVR: %s", gvrKey)
-	}
-
-	// this is worth a panic if it fails...
-	handlerFunc, ok := genericHandler.(Handler)
-	if !ok {
-		return fmt.Errorf("invalid handler type for GVR: %s", gvrKey)
-	}
-
-	nn := types.NamespacedName{}
-	if parts := strings.Split(oi.NamespacedKey, "/"); len(parts) == 1 {
-		nn.Name = parts[0]
-	} else {
-		nn.Namespace = parts[0]
-		nn.Name = parts[1]
-	}
-	err := handlerFunc(ctx, ctrl.Request{NamespacedName: nn})
+	err := handler(ctx, ctrl.Request{NamespacedName: oi.NamespacedName})
 	if err != nil {
 		handlerErrorsTotal.WithLabelValues(gvrKey).Inc()
 	}
 	return err
 }
 
-// shutdown performs a graceful shutdown of the controller.
-func (dc *DynamicController) shutdown(ctx context.Context) error {
-	dc.log.Info("Starting graceful shutdown")
-
-	var wg sync.WaitGroup
-	dc.informers.Range(func(key, value interface{}) bool {
-		k := key.(schema.GroupVersionResource)
-		dc.log.V(1).Info("Shutting down informer", "gvr", k.String())
-		wg.Add(1)
-		go func(informer *informerWrapper) {
-			defer wg.Done()
-			informer.informer.Shutdown()
-		}(value.(*informerWrapper))
-		return true
-	})
-
-	// Wait for all informers to shut down or timeout
-	done := make(chan struct{})
-	go func() {
-		wg.Wait()
-		close(done)
-	}()
-
-	select {
-	case <-done:
-		dc.log.Info("All informers shut down successfully")
-	case <-ctx.Done():
-		dc.log.Error(ctx.Err(), "Timeout waiting for informers to shut down")
-		return ctx.Err()
+func (dc *DynamicController) enqueueParent(parentGVR schema.GroupVersionResource, obj interface{}, eventType string) {
+	mobj, err := meta.Accessor(obj)
+	if err != nil {
+		dc.log.Error(err, "Failed to get meta for object to enqueue", "eventType", eventType)
+		return
 	}
 
-	return nil
-}
+	oi := ObjectIdentifiers{NamespacedName: types.NamespacedName{
+		Namespace: mobj.GetNamespace(),
+		Name:      mobj.GetName(),
+	}, GVR: parentGVR}
+	dc.log.V(1).Info("Enqueueing object", "objectIdentifiers", oi, "eventType", eventType)
 
-// ObjectIdentifiers is a struct that holds the namespaced key and the GVR of the object.
-//
-// Since we are handling all the resources using the same handlerFunc, we need to know
-// what GVR we're dealing with - so that we can use the appropriate workflow operator.
-type ObjectIdentifiers struct {
-	// NamespacedKey is the namespaced key of the object. Typically in the format
-	// `namespace/name`.
-	NamespacedKey string
-	GVR           schema.GroupVersionResource
+	informerEventsTotal.WithLabelValues(parentGVR.String(), eventType).Inc()
+	dc.queue.Add(oi)
 }
 
-// updateFunc is the update event handler for the GVR informers
-func (dc *DynamicController) updateFunc(old, new interface{}) {
-	newObj, ok := new.(*unstructured.Unstructured)
-	if !ok {
-		dc.log.Error(nil, "failed to cast new object to unstructured")
+func (dc *DynamicController) updateFunc(parentGVR schema.GroupVersionResource, oldObj, newObj interface{}) {
+	newMeta, err := meta.Accessor(newObj)
+	if err != nil {
+		dc.log.Error(err, "failed to access new object meta")
 		return
 	}
-	oldObj, ok := old.(*unstructured.Unstructured)
-	if !ok {
-		dc.log.Error(nil, "failed to cast old object to unstructured")
+	oldMeta, err := meta.Accessor(oldObj)
+	if err != nil {
+		dc.log.Error(err, "failed to access old object meta")
 		return
 	}
-
-	if newObj.GetGeneration() == oldObj.GetGeneration() {
+	if newMeta.GetGeneration() == oldMeta.GetGeneration() {
 		dc.log.V(2).Info("Skipping update due to unchanged generation",
-			"name", newObj.GetName(),
-			"namespace", newObj.GetNamespace(),
-			"generation", newObj.GetGeneration())
+			"name", newMeta.GetName(), "namespace", newMeta.GetNamespace(), "generation", newMeta.GetGeneration())
 		return
 	}
-
-	dc.enqueueObject(new, "update")
+	dc.enqueueParent(parentGVR, newObj, eventTypeUpdate)
 }
 
-// enqueueObject adds an object to the workqueue
-func (dc *DynamicController) enqueueObject(obj interface{}, eventType string) {
-	namespacedKey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
-	if err != nil {
-		dc.log.Error(err, "Failed to get key for object", "eventType", eventType)
-		return
+// Register registers parent and children via reconciliation.
+func (dc *DynamicController) Register(
+	_ context.Context,
+	parent schema.GroupVersionResource,
+	instanceHandler Handler,
+	resourceGVRsToWatch ...schema.GroupVersionResource,
+) error {
+	dc.mu.Lock()
+	defer dc.mu.Unlock()
+
+	reg, exists := dc.registrations[parent]
+	if !exists {
+		reg = &registration{
+			parentGVR:       parent,
+			childHandlerIDs: make(map[schema.GroupVersionResource]string),
+		}
+		dc.registrations[parent] = reg
 	}
 
-	u, ok := obj.(*unstructured.Unstructured)
-	if !ok {
-		err := fmt.Errorf("object is not an Unstructured")
-		dc.log.Error(err, "Failed to cast object to Unstructured", "eventType", eventType, "namespacedKey", namespacedKey)
-		return
+	if err := dc.reconcileParentLocked(parent, instanceHandler, reg); err != nil {
+		return err
+	}
+	if err := dc.reconcileChildrenLocked(parent, resourceGVRsToWatch, reg); err != nil {
+		return err
 	}
 
-	gvk := u.GroupVersionKind()
-	gvr := metadata.GVKtoGVR(gvk)
+	// kick reconciliation for existing parent objects
+	if w, ok := dc.watches[parent]; ok && !w.Informer().IsStopped() {
+		// Use informer cache if running to repopulate the queue.
+		objects := w.Informer().GetStore().List()
+		for _, obj := range objects {
+			dc.enqueueParent(parent, obj, "update")
+		}
+	}
+
+	dc.log.V(1).Info("Successfully registered GVR", "gvr", keyFromGVR(parent))
+	return nil
+}
+
+// Deregister clears parent and children and drops any queued items for the parent GVR.
+func (dc *DynamicController) Deregister(_ context.Context, parent schema.GroupVersionResource) error {
+	dc.mu.Lock()
+	defer dc.mu.Unlock()
 
-	objectIdentifiers := ObjectIdentifiers{
-		NamespacedKey: namespacedKey,
-		GVR:           gvr,
+	reg, exists := dc.registrations[parent]
+	if !exists {
+		return nil
+	}
+
+	gvrKey := keyFromGVR(parent)
+
+	if err := dc.reconcileChildrenLocked(parent, nil, reg); err != nil {
+		dc.log.Error(err, "failed to detach children", "parent", gvrKey)
+	}
+	if err := dc.reconcileParentLocked(parent, nil, reg); err != nil {
+		dc.log.Error(err, "failed to detach parent", "parent", gvrKey)
 	}
 
-	dc.log.V(1).Info("Enqueueing object",
-		"objectIdentifiers", objectIdentifiers,
-		"eventType", eventType)
+	delete(dc.registrations, parent)
 
-	informerEventsTotal.WithLabelValues(gvr.String(), eventType).Inc()
-	dc.queue.Add(objectIdentifiers)
+	dc.log.V(1).Info("Successfully unregistered GVR", "gvr", gvrKey)
+	return nil
 }
 
-// Register registers a new GVK to the informers map safely.
-func (dc *DynamicController) Register(ctx context.Context, gvr schema.GroupVersionResource, handler Handler) error {
-	dc.log.V(1).Info("Registering new GVK", "gvr", gvr)
-
-	_, exists := dc.informers.Load(gvr)
-	if exists {
-		// Even thought the informer is already registered, we should still
-		// update the handler, as it might have changed.
-		dc.handlers.Store(gvr, handler)
-		// trigger reconciliation of the corresponding gvr's
-		objs, err := dc.kubeClient.Resource(gvr).Namespace("").List(ctx, metav1.ListOptions{})
-		if err != nil {
-			return fmt.Errorf("failed to list objects for GVR %s: %w", gvr, err)
+// ----- internal helpers -----
+
+func (dc *DynamicController) ensureWatchLocked(
+	gvr schema.GroupVersionResource,
+) *internal.LazyInformer {
+	if w, ok := dc.watches[gvr]; ok {
+		return w
+	}
+
+	// Create per-GVR watch wrapper (informer created lazily on first handler)
+	w := internal.NewLazyInformer(dc.client, gvr, dc.config.ResyncPeriod, nil, dc.log)
+	dc.watches[gvr] = w
+	return w
+}
+
+// reconcileParentLocked ensures a parent watch exists and has exactly one handler.
+// If instanceHandler is nil, the parent handler is removed.
+// Must be called with dc.mu held.
+func (dc *DynamicController) reconcileParentLocked(
+	parent schema.GroupVersionResource,
+	instanceHandler Handler,
+	reg *registration,
+) error {
+	if instanceHandler == nil {
+		if reg.parentHandlerID == "" {
+			return nil
 		}
-		for _, obj := range objs.Items {
-			dc.enqueueObject(&obj, "update")
+		// remove parent handler if present
+		if err := dc.removeHandlerLocked(parent, reg.parentHandlerID); err != nil {
+			return fmt.Errorf("removing parent handler %s: %w", parent, err)
 		}
+		reg.parentHandlerID = ""
+		dc.handlers.Delete(parent)
+
+		gvrCount.Dec()
+		handlerDetachTotal.WithLabelValues("parent").Inc()
+		handlerCount.WithLabelValues("parent").Dec()
+		dc.log.V(1).Info("Detached parent", "gvr", parent)
 		return nil
 	}
 
-	// Create a new informer
-	gvkInformer := dynamicinformer.NewFilteredDynamicSharedInformerFactory(
-		dc.kubeClient,
-		dc.config.ResyncPeriod,
-		// Maybe we can make this configurable in the future. Thinking that
-		// we might want to filter out some resources, by namespace or labels
-		"",
-		nil,
-	)
-	informer := gvkInformer.ForResource(gvr).Informer()
-
-	// Set up event handlers
-	_, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
-		AddFunc:    func(obj interface{}) { dc.enqueueObject(obj, "add") },
-		UpdateFunc: dc.updateFunc,
-		DeleteFunc: func(obj interface{}) { dc.enqueueObject(obj, "delete") },
-	})
-	if err != nil {
-		dc.log.Error(err, "Failed to add event handler", "gvr", gvr)
-		return fmt.Errorf("failed to add event handler for GVR %s: %w", gvr, err)
-	}
-	if err := informer.SetWatchErrorHandler(func(r *cache.Reflector, err error) {
-		dc.log.Error(err, "Watch error", "gvr", gvr)
-	}); err != nil {
-		dc.log.Error(err, "Failed to set watch error handler", "gvr", gvr)
-		return fmt.Errorf("failed to set watch error handler for GVR %s: %w", gvr, err)
+	// ensure watch
+	w := dc.ensureWatchLocked(parent)
+
+	// create handler if missing
+	if reg.parentHandlerID == "" {
+		parentHandlerID := parentHandlerID(parent)
+		if err := w.AddHandler(dc.ctx, parentHandlerID, cache.ResourceEventHandlerFuncs{
+			AddFunc:    func(obj interface{}) { dc.enqueueParent(parent, obj, eventTypeAdd) },
+			UpdateFunc: func(oldObj, newObj interface{}) { dc.updateFunc(parent, oldObj, newObj) },
+			DeleteFunc: func(obj interface{}) { dc.enqueueParent(parent, obj, eventTypeDelete) },
+		}); err != nil {
+			return fmt.Errorf("add parent handler %s: %w", parent, err)
+		}
+		reg.parentHandlerID = parentHandlerID
+
+		gvrCount.Inc()
+		handlerAttachTotal.WithLabelValues("parent").Inc()
+		handlerCount.WithLabelValues("parent").Inc()
+		dc.log.V(1).Info("Attached parent", "gvr", parent)
 	}
-	dc.handlers.Store(gvr, handler)
 
-	informerContext := context.Background()
-	cancelableContext, cancel := context.WithCancel(informerContext)
-	// Start the informer
-	go func() {
-		dc.log.V(1).Info("Starting informer", "gvr", gvr)
-		// time.Sleep(5 * time.Millisecond)
-		informer.Run(cancelableContext.Done())
-	}()
+	// always update latest handler
+	dc.handlers.Store(parent, instanceHandler)
 
-	dc.log.V(1).Info("Waiting for cache sync", "gvr", gvr)
-	startTime := time.Now()
-	// Wait for cache sync with a timeout
-	synced := cache.WaitForCacheSync(cancelableContext.Done(), informer.HasSynced)
-	syncDuration := time.Since(startTime)
-	informerSyncDuration.WithLabelValues(gvr.String()).Observe(syncDuration.Seconds())
-
-	if !synced {
-		cancel()
-		return fmt.Errorf("failed to sync informer cache for GVR %s", gvr)
-	}
-
-	dc.informers.Store(gvr, &informerWrapper{
-		informer: gvkInformer,
-		shutdown: cancel,
-	})
-	gvrCount.Inc()
-	dc.log.V(1).Info("Successfully registered GVK", "gvr", gvr)
 	return nil
 }
 
-// Deregister safely removes a GVK from the controller and cleans up associated resources.
-func (dc *DynamicController) Deregister(ctx context.Context, gvr schema.GroupVersionResource) error {
-	dc.log.Info("Unregistering GVK", "gvr", gvr)
+// reconcileChildrenLocked ensures that reg.childHandlerIDs matches the desired set.
+// Must be called with dc.mu held.
+func (dc *DynamicController) reconcileChildrenLocked(
+	parent schema.GroupVersionResource,
+	desired []schema.GroupVersionResource,
+	reg *registration,
+) error {
+	desiredSet := make(map[schema.GroupVersionResource]struct{}, len(desired))
+	for _, g := range desired {
+		desiredSet[g] = struct{}{}
+	}
+
+	parentGVRKey := keyFromGVR(parent)
+
+	// remove obsolete
+	for child, childHandlerID := range reg.childHandlerIDs {
+		if _, keep := desiredSet[child]; keep {
+			continue
+		}
+		if err := dc.removeHandlerLocked(child, childHandlerID); err != nil {
+			return fmt.Errorf("removing child handler %s: %w", child, err)
+		}
+		delete(reg.childHandlerIDs, child)
+
+		childGVRKey := keyFromGVR(child)
+		handlerDetachTotal.WithLabelValues("child").Inc()
+		handlerCount.WithLabelValues("child").Dec()
+		dc.log.V(1).Info("Detached child", "parent", parentGVRKey, "gvr", childGVRKey)
+	}
 
-	// Retrieve the informer
-	informerObj, ok := dc.informers.Load(gvr)
+	// add missing
+	for child := range desiredSet {
+		if _, exists := reg.childHandlerIDs[child]; exists {
+			continue
+		}
+		w := dc.ensureWatchLocked(child)
+		childHandlerID := childHandlerID(parent, child)
+		if err := w.AddHandler(dc.ctx, childHandlerID, dc.handlerForChildGVR(parent, child)); err != nil {
+			return fmt.Errorf("add child handler %s: %w", child, err)
+		}
+		reg.childHandlerIDs[child] = childHandlerID
+
+		childGVRKey := keyFromGVR(child)
+		handlerAttachTotal.WithLabelValues("child").Inc()
+		handlerCount.WithLabelValues("child").Inc()
+		dc.log.V(1).Info("Attached child", "parent", parentGVRKey, "gvr", childGVRKey)
+	}
+
+	return nil
+}
+
+// removeHandlerLocked removes a handler with the specified ID for a given GroupVersionResource watch.
+// If the handler removal stops the watch, the corresponding GroupVersionResource entry is deleted from the watches map.
+// Returns an error if the handler removal fails.
+func (dc *DynamicController) removeHandlerLocked(gvr schema.GroupVersionResource, handlerID string) error {
+	w, ok := dc.watches[gvr]
 	if !ok {
-		dc.log.V(1).Info("GVK not registered, nothing to deregister", "gvr", gvr)
+		// no watch means there cannot be a handler
 		return nil
 	}
+	stopped, err := w.RemoveHandler(handlerID)
+	if err != nil {
+		return fmt.Errorf("removing handler %s for watch on %s failed: %w", handlerID, gvr, err)
+	}
+	if stopped {
+		delete(dc.watches, gvr)
+	}
+	return nil
+}
 
-	wrapper, ok := informerObj.(*informerWrapper)
-	if !ok {
-		return fmt.Errorf("invalid informer type for GVR: %s", gvr)
+func (dc *DynamicController) handlerForChildGVR(parent, child schema.GroupVersionResource) cache.ResourceEventHandler {
+	parentGVRKey, childGVRKey := keyFromGVR(parent), keyFromGVR(child)
+	handle := func(obj interface{}, eventType string) {
+		objMeta, err := meta.Accessor(obj)
+		if err != nil {
+			dc.log.Error(err, "failed to get metadata accessor for object", "eventType", eventType)
+			return
+		}
+		lbls := objMeta.GetLabels()
+		owned, ok := lbls[metadata.OwnedLabel]
+		if !ok || owned != "true" {
+			return
+		}
+		name, ok := lbls[metadata.InstanceLabel]
+		if !ok {
+			return
+		}
+		namespace, ok := lbls[metadata.InstanceNamespaceLabel]
+		if !ok {
+			return
+		}
+
+		parentGVK, err := dc.mapper.KindFor(parent)
+		if err != nil {
+			dc.log.Error(err, "failed to get parent GVK", "parent", parentGVRKey)
+			return
+		}
+		pom := &metav1.PartialObjectMetadata{}
+		pom.SetGroupVersionKind(parentGVK)
+		pom.SetName(name)
+		pom.SetNamespace(namespace)
+
+		dc.log.V(1).Info("Child triggered parent reconciliation",
+			"parent", parentGVRKey,
+			"child", childGVRKey,
+			"eventType", eventType,
+			"childName", objMeta.GetName(),
+			"childNamespace", objMeta.GetNamespace(),
+			"targetName", name,
+			"targetNamespace", namespace,
+		)
+		dc.enqueueParent(parent, pom, eventType)
+	}
+	return cache.ResourceEventHandlerFuncs{
+		AddFunc:    func(obj interface{}) { handle(obj, eventTypeAdd) },
+		UpdateFunc: func(oldObj, newObj interface{}) { handle(newObj, eventTypeUpdate) },
+		DeleteFunc: func(obj interface{}) { handle(obj, eventTypeDelete) },
+	}
+}
+
+func (dc *DynamicController) gracefulShutdown() error {
+	dc.log.Info("Starting graceful shutdown")
+
+	dc.mu.Lock()
+	for gvr, w := range dc.watches {
+		dc.log.V(1).Info("Stopping watch", "gvr", keyFromGVR(gvr))
+		w.Shutdown()
 	}
+	dc.mu.Unlock()
 
-	// Stop the informer
-	dc.log.V(1).Info("Stopping informer", "gvr", gvr)
+	queueShutdownDone := make(chan struct{})
+	go func() {
+		dc.queue.ShutDown()
+		close(queueShutdownDone)
+	}()
 
-	// Cancel the context to stop the informer
-	wrapper.shutdown()
-	// Wait for the informer to shut down
-	wrapper.informer.Shutdown()
+	ctx := context.Background()
+	var cancel context.CancelFunc
+	if dc.config.QueueShutdownTimeout > 0 {
+		ctx, cancel = context.WithTimeout(ctx, dc.config.QueueShutdownTimeout)
+	} else {
+		ctx, cancel = context.WithCancel(ctx)
+	}
+	defer cancel()
 
-	// Remove the informer from the map
-	dc.informers.Delete(gvr)
+	select {
+	case <-queueShutdownDone:
+		return nil
+	case <-ctx.Done():
+		return fmt.Errorf("timeout waiting for queue to shutdown: %w", ctx.Err())
+	}
+}
+
+// keyFromGVR returns a compact, allocation-efficient string key for the given
+// GroupVersionResource in the canonical "group/version/resource" format.
+// Unlike [schema.GroupVersionResource.String], it omits labels and avoids extra
+// allocations, making it suitable for use as map keys, metrics labels, and cache identifiers.
+func keyFromGVR(gvr schema.GroupVersionResource) string {
+	var b strings.Builder
+	if gvr.Group != "" {
+		b.WriteString(gvr.Group)
+	}
+	if gvr.Version != "" {
+		b.WriteRune('/')
+		b.WriteString(gvr.Version)
+	}
+	if gvr.Resource != "" {
+		b.WriteRune('/')
+		b.WriteString(gvr.Resource)
+	}
+	return b.String()
+}
 
-	// Unregister the handler if any
-	dc.handlers.Delete(gvr)
+func childHandlerID(parent schema.GroupVersionResource, child schema.GroupVersionResource) string {
+	return "child:" + keyFromGVR(parent) + "->" + keyFromGVR(child)
+}
 
-	gvrCount.Dec()
-	// Clean up any pending items in the queue for this GVR
-	// NOTE(a-hilaly): This is a bit heavy.. maybe we can find a better way to do this.
-	// Thinking that we might want to have a queue per GVR.
-	// dc.cleanupQueue(gvr)
-	// time.Sleep(1 * time.Second)
-	// isStopped := wrapper.informer.ForResource(gvr).Informer().IsStopped()
-	dc.log.V(1).Info("Successfully unregistered GVK", "gvr", gvr)
-	return nil
+func parentHandlerID(parent schema.GroupVersionResource) string {
+	handlerID := "parent:" + keyFromGVR(parent)
+	return handlerID
 }
diff --git a/pkg/dynamiccontroller/internal/gvr_watch.go b/pkg/dynamiccontroller/internal/gvr_watch.go
new file mode 100644
index 0000000..e357b3e
--- /dev/null
+++ b/pkg/dynamiccontroller/internal/gvr_watch.go
@@ -0,0 +1,172 @@
+// Copyright 2025 The Kube Resource Orchestrator Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package internal
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/go-logr/logr"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/metadata"
+	"k8s.io/client-go/metadata/metadatainformer"
+	"k8s.io/client-go/tools/cache"
+)
+
+// LazyInformer manages a SharedIndexInformer per GVR with multiple handlers.
+// It lazily starts when the first handler is added and stops when the last is removed.
+// It can restart again after a full shutdown.
+type LazyInformer struct {
+	gvr    schema.GroupVersionResource
+	client metadata.Interface
+	resync time.Duration
+	tweak  metadatainformer.TweakListOptionsFunc
+
+	mu       sync.Mutex
+	informer cache.SharedIndexInformer
+	handlers map[string]cache.ResourceEventHandlerRegistration
+
+	done   <-chan struct{}
+	cancel context.CancelFunc
+
+	log logr.Logger
+}
+
+func NewLazyInformer(
+	client metadata.Interface,
+	gvr schema.GroupVersionResource,
+	resync time.Duration,
+	tweak metadatainformer.TweakListOptionsFunc,
+	logger logr.Logger,
+) *LazyInformer {
+	li := &LazyInformer{
+		gvr:      gvr,
+		client:   client,
+		resync:   resync,
+		tweak:    tweak,
+		handlers: make(map[string]cache.ResourceEventHandlerRegistration),
+		log:      logger.WithValues("gvr", gvr.String()),
+	}
+	return li
+}
+
+func (w *LazyInformer) resetContext(parent context.Context) {
+	ctx, cancel := context.WithCancel(parent)
+	w.done = ctx.Done()
+	w.cancel = cancel
+}
+
+// ensureInformer initializes informer if missing or after shutdown.
+func (w *LazyInformer) ensureInformer() {
+	if w.informer != nil {
+		return
+	}
+	inf := metadatainformer.NewFilteredMetadataInformer(
+		w.client, w.gvr, metav1.NamespaceAll, w.resync,
+		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
+		w.tweak,
+	).Informer()
+
+	_ = inf.SetWatchErrorHandler(func(_ *cache.Reflector, err error) {
+		w.log.V(1).Error(err, "watch error for lazy informer", "gvr", w.gvr)
+	})
+
+	w.informer = inf
+}
+
+// AddHandler registers a new event handler and starts the informer if needed.
+func (w *LazyInformer) AddHandler(ctx context.Context, id string, h cache.ResourceEventHandler) error {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	// If informer was fully stopped, reset context before re-creating it.
+	if w.cancel == nil {
+		// recreate context
+		w.resetContext(ctx)
+	}
+
+	w.ensureInformer()
+
+	reg, err := w.informer.AddEventHandler(h)
+	if err != nil {
+		return err
+	}
+	w.handlers[id] = reg
+
+	// Start informer if first handler
+	if len(w.handlers) == 1 {
+		go w.run()
+
+		if !cache.WaitForCacheSync(w.done, w.informer.HasSynced) {
+			w.log.Error(fmt.Errorf("cache sync failed"), "lazy informer sync failure", "gvr", w.gvr)
+			w.cancel()
+			w.cancel = nil
+			w.informer = nil
+			w.handlers = make(map[string]cache.ResourceEventHandlerRegistration)
+			return fmt.Errorf("failed to sync informer for %s", w.gvr)
+		}
+	}
+	return nil
+}
+
+func (w *LazyInformer) run() {
+	w.informer.Run(w.done)
+}
+
+// RemoveHandler unregisters a handler. Returns true if informer was stopped.
+func (w *LazyInformer) RemoveHandler(id string) (bool, error) {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	reg, ok := w.handlers[id]
+	if !ok {
+		return false, nil
+	}
+	if w.informer != nil {
+		if err := w.informer.RemoveEventHandler(reg); err != nil {
+			return false, err
+		}
+		delete(w.handlers, id)
+	}
+
+	// if we now have no handlers and we had a running informer, stop it.
+	if len(w.handlers) == 0 && w.informer != nil {
+		w.cancel()
+		<-w.done
+		w.cancel = nil
+		w.informer = nil
+		return true, nil
+	}
+	return false, nil
+}
+
+// Informer returns the underlying SharedIndexInformer (may be nil).
+func (w *LazyInformer) Informer() cache.SharedIndexInformer {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	return w.informer
+}
+
+// Shutdown stops the informer and clears state.
+func (w *LazyInformer) Shutdown() {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	w.cancel()
+	w.informer = nil
+	w.handlers = make(map[string]cache.ResourceEventHandlerRegistration)
+}
diff --git a/pkg/dynamiccontroller/metrics.go b/pkg/dynamiccontroller/metrics.go
index aba10df..ac6f766 100644
--- a/pkg/dynamiccontroller/metrics.go
+++ b/pkg/dynamiccontroller/metrics.go
@@ -28,6 +28,9 @@ func init() {
 		reconcileDuration,
 		gvrCount,
 		queueLength,
+		handlerCount,
+		handlerAttachTotal,
+		handlerDetachTotal,
 		handlerErrorsTotal,
 		informerSyncDuration,
 		informerEventsTotal,
@@ -63,7 +66,7 @@ var (
 	gvrCount = prometheus.NewGauge(
 		prometheus.GaugeOpts{
 			Name: "dynamic_controller_gvr_count",
-			Help: "Number of GVRs currently managed by the controller",
+			Help: "Number of Instance GVRs currently managed by the controller",
 		},
 	)
 	queueLength = prometheus.NewGauge(
@@ -72,6 +75,18 @@ var (
 			Help: "Current length of the workqueue",
 		},
 	)
+	handlerCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+		Name: "dynamic_controller_handler_count_total",
+		Help: "Number of active handlers used for distributing events to instance controllers",
+	}, []string{"type"})
+	handlerAttachTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "dynamic_controller_handler_attach_total",
+		Help: "Total number of handler attachments by handler type",
+	}, []string{"type"})
+	handlerDetachTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "dynamic_controller_handler_detach_total",
+		Help: "Total number of handler detachments by handler type",
+	}, []string{"type"})
 	handlerErrorsTotal = prometheus.NewCounterVec(
 		prometheus.CounterOpts{
 			Name: "dynamic_controller_handler_errors_total",
diff --git a/pkg/graph/builder.go b/pkg/graph/builder.go
index 91f0be4..72cf2e7 100644
--- a/pkg/graph/builder.go
+++ b/pkg/graph/builder.go
@@ -20,7 +20,6 @@ import (
 	"slices"
 
 	"github.com/google/cel-go/cel"
-	"github.com/google/cel-go/common/types/ref"
 	"golang.org/x/exp/maps"
 	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -28,6 +27,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/yaml"
 	"k8s.io/apiserver/pkg/cel/openapi/resolver"
 	"k8s.io/client-go/rest"
+	"k8s.io/kube-openapi/pkg/validation/spec"
 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 
 	"github.com/kubernetes-sigs/kro/api/v1alpha1"
@@ -35,7 +35,6 @@ import (
 	"github.com/kubernetes-sigs/kro/pkg/cel/ast"
 	"github.com/kubernetes-sigs/kro/pkg/graph/crd"
 	"github.com/kubernetes-sigs/kro/pkg/graph/dag"
-	"github.com/kubernetes-sigs/kro/pkg/graph/emulator"
 	"github.com/kubernetes-sigs/kro/pkg/graph/parser"
 	"github.com/kubernetes-sigs/kro/pkg/graph/schema"
 	schemaresolver "github.com/kubernetes-sigs/kro/pkg/graph/schema/resolver"
@@ -58,12 +57,9 @@ func NewBuilder(
 		return nil, fmt.Errorf("failed to create dynamic REST mapper: %w", err)
 	}
 
-	resourceEmulator := emulator.NewEmulator()
-
 	rgBuilder := &Builder{
-		resourceEmulator: resourceEmulator,
-		schemaResolver:   schemaResolver,
-		restMapper:       rm,
+		schemaResolver: schemaResolver,
+		restMapper:     rm,
 	}
 	return rgBuilder, nil
 }
@@ -94,14 +90,7 @@ func NewBuilder(
 type Builder struct {
 	// schemaResolver is used to resolve the OpenAPI schema for the resources.
 	schemaResolver resolver.SchemaResolver
-	// resourceEmulator is used to emulate the resources. This is used to validate
-	// the CEL expressions in the resources. Because looking up the CEL expressions
-	// isn't enough for kro to validate the expressions.
-	//
-	// Maybe there is a better way, if anything probably there is a better way to
-	// validate the CEL expressions. To revisit.
-	resourceEmulator *emulator.Emulator
-	restMapper       meta.RESTMapper
+	restMapper     meta.RESTMapper
 }
 
 // NewResourceGraphDefinition creates a new ResourceGraphDefinition object from the given ResourceGraphDefinition
@@ -203,39 +192,70 @@ func (b *Builder) NewResourceGraphDefinition(originalCR *v1alpha1.ResourceGraphD
 		return nil, fmt.Errorf("failed to build resourcegraphdefinition '%v': %w", rgd.Name, err)
 	}
 
-	// Before getting into the dependency graph, we need to validate the CEL expressions
-	// in the instance resource.
-	// To do that, we need to isolate each resource
-	// and evaluate the CEL expressions in the context of the resource graph definition.
-	// This is done
-	// by dry-running the CEL expressions against the emulated resources.
-	err = validateResourceCELExpressions(resources, instance)
-	if err != nil {
-		return nil, fmt.Errorf("failed to validate resource CEL expressions: %w", err)
+	// collect all OpenAPI schemas for CEL type checking. This map will be used to
+	// create a typed CEL environment that validates expressions against the actual
+	// resource schemas.
+	schemas := make(map[string]*spec.Schema)
+	for id, resource := range resources {
+		if resource.schema != nil {
+			schemas[id] = resource.schema
+		}
 	}
 
-	// Now that we have the instance resource, we can move into the next stage of
-	// building the resource graph definition. Understanding the relationships between the
-	// resources in the resource graph definition a.k.a the dependency graph.
+	// include the instance spec schema in the context as "schema". This will let us
+	// validate expressions such as ${schema.spec.someField}.
 	//
-	// The dependency graph is a directed acyclic graph that represents the
-	// relationships between the resources in the resource graph definition. The graph is
-	// used to determine the order in which the resources should be created in the
-	// cluster.
+	// not that we only include the spec and metadata fields, instance status references
+	// are not allowed in RGDs (yet)
+	schemaWithoutStatus, err := getSchemaWithoutStatus(instance.crd)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get schema without status: %w", err)
+	}
+	schemas["schema"] = schemaWithoutStatus
+
+	// First, build the dependency graph by inspecting CEL expressions.
+	// This extracts all resource dependencies and validates that:
+	// 1. All referenced resources are defined in the RGD
+	// 2. There are no unknown functions
+	// 3. The dependency graph is acyclic
 	//
-	// The dependency graph is built by inspecting the CEL expressions in the
-	// resources and the instance resource, using a CEL AST (Abstract Syntax Tree)
-	// inspector.
+	// We do this BEFORE type checking so that undeclared resource errors
+	// are caught here with clear messages, rather than as CEL type errors.
 	dag, err := b.buildDependencyGraph(resources)
 	if err != nil {
 		return nil, fmt.Errorf("failed to build dependency graph: %w", err)
 	}
-
+	// Ensure the graph is acyclic and get the topological order of resources.
 	topologicalOrder, err := dag.TopologicalSort()
 	if err != nil {
 		return nil, fmt.Errorf("failed to get topological order: %w", err)
 	}
 
+	// Now that we know all resources are properly declared and dependencies are valid,
+	// we can perform type checking on the CEL expressions.
+
+	// Create a typed CEL environment with all resource schemas for template expressions
+	templatesEnv, err := krocel.TypedEnvironment(schemas)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create typed CEL environment: %w", err)
+	}
+
+	// Create a CEL environment with only "schema" for includeWhen expressions
+	var schemaEnv *cel.Env
+	if schemas["schema"] != nil {
+		schemaEnv, err = krocel.TypedEnvironment(map[string]*spec.Schema{"schema": schemas["schema"]})
+		if err != nil {
+			return nil, fmt.Errorf("failed to create CEL environment for includeWhen validation: %w", err)
+		}
+	}
+
+	// Validate all CEL expressions for each resource node
+	for _, resource := range resources {
+		if err := validateNode(resource, templatesEnv, schemaEnv, schemas[resource.id]); err != nil {
+			return nil, fmt.Errorf("failed to validate node %q: %w", resource.id, err)
+		}
+	}
+
 	resourceGraphDefinition := &Graph{
 		DAG:              dag,
 		Instance:         instance,
@@ -301,8 +321,7 @@ func (b *Builder) buildRGResource(
 		return nil, fmt.Errorf("failed to get schema for resource %s: %w", rgResource.ID, err)
 	}
 
-	var emulatedResource *unstructured.Unstructured
-	var resourceVariables []*variable.ResourceField
+	var templateVariables []*variable.ResourceField
 
 	// TODO(michaelhtm): CRDs are not supported for extraction currently
 	// implement new logic specific to CRDs
@@ -315,21 +334,13 @@ func (b *Builder) buildRGResource(
 			return nil, fmt.Errorf("failed, CEL expressions are not supported for CRDs, resource %s", rgResource.ID)
 		}
 	} else {
-
-		// 4. Emulate the resource, this is later used to verify the validity of the
-		//    CEL expressions.
-		emulatedResource, err = b.resourceEmulator.GenerateDummyCR(gvk, resourceSchema)
-		if err != nil {
-			return nil, fmt.Errorf("failed to generate dummy CR for resource %s: %w", rgResource.ID, err)
-		}
-
 		// 5. Extract CEL fieldDescriptors from the schema.
 		fieldDescriptors, err := parser.ParseResource(resourceObject, resourceSchema)
 		if err != nil {
 			return nil, fmt.Errorf("failed to extract CEL expressions from schema for resource %s: %w", rgResource.ID, err)
 		}
 		for _, fieldDescriptor := range fieldDescriptors {
-			resourceVariables = append(resourceVariables, &variable.ResourceField{
+			templateVariables = append(templateVariables, &variable.ResourceField{
 				// Assume variables are static; we'll validate them later
 				Kind:            variable.ResourceVariableKindStatic,
 				FieldDescriptor: fieldDescriptor,
@@ -359,9 +370,8 @@ func (b *Builder) buildRGResource(
 		id:                     rgResource.ID,
 		gvr:                    mapping.Resource,
 		schema:                 resourceSchema,
-		emulatedObject:         emulatedResource,
 		originalObject:         &unstructured.Unstructured{Object: resourceObject},
-		variables:              resourceVariables,
+		variables:              templateVariables,
 		readyWhenExpressions:   readyWhen,
 		includeWhenExpressions: includeWhen,
 		namespaced:             mapping.Scope.Name() == meta.RESTScopeNameNamespace,
@@ -406,15 +416,8 @@ func (b *Builder) buildDependencyGraph(
 	}
 
 	for _, resource := range resources {
-		for _, resourceVariable := range resource.variables {
-			for _, expression := range resourceVariable.Expressions {
-				// We need to inspect the expression to understand how it relates to the
-				// resources defined in the resource graph definition.
-				err := validateCELExpressionContext(env, expression, resourceNames)
-				if err != nil {
-					return nil, fmt.Errorf("failed to validate expression context: %w", err)
-				}
-
+		for _, templateVariable := range resource.variables {
+			for _, expression := range templateVariable.Expressions {
 				// We need to extract the dependencies from the expression.
 				resourceDependencies, isStatic, err := extractDependencies(env, expression, resourceNames)
 				if err != nil {
@@ -423,14 +426,14 @@ func (b *Builder) buildDependencyGraph(
 
 				// Static until proven dynamic.
 				//
-				// This reads as: If the expression is dynamic and the resource variable is
-				// static, then we need to mark the resource variable as dynamic.
-				if !isStatic && resourceVariable.Kind == variable.ResourceVariableKindStatic {
-					resourceVariable.Kind = variable.ResourceVariableKindDynamic
+				// This reads as: If the expression is dynamic and the template variable is
+				// static, then we need to mark the template variable as dynamic.
+				if !isStatic && templateVariable.Kind == variable.ResourceVariableKindStatic {
+					templateVariable.Kind = variable.ResourceVariableKindDynamic
 				}
 
 				resource.addDependencies(resourceDependencies...)
-				resourceVariable.AddDependencies(resourceDependencies...)
+				templateVariable.AddDependencies(resourceDependencies...)
 				// We need to add the dependencies to the graph.
 				if err := directedAcyclicGraph.AddDependencies(resource.id, resourceDependencies); err != nil {
 					return nil, err
@@ -479,16 +482,11 @@ func (b *Builder) buildInstanceResource(
 	overrideStatusFields := true
 	instanceCRD := crd.SynthesizeCRD(group, apiVersion, kind, *instanceSpecSchema, *instanceStatusSchema, overrideStatusFields, rgDefinition.AdditionalPrinterColumns)
 
-	// Emulate the CRD
 	instanceSchemaExt := instanceCRD.Spec.Versions[0].Schema.OpenAPIV3Schema
 	instanceSchema, err := schema.ConvertJSONSchemaPropsToSpecSchema(instanceSchemaExt)
 	if err != nil {
 		return nil, fmt.Errorf("failed to convert JSON schema to spec schema: %w", err)
 	}
-	emulatedInstance, err := b.resourceEmulator.GenerateDummyCR(gvk, instanceSchema)
-	if err != nil {
-		return nil, fmt.Errorf("failed to generate dummy CR for instance: %w", err)
-	}
 
 	resourceNames := maps.Keys(resources)
 	env, err := krocel.DefaultEnvironment(krocel.WithResourceIDs(resourceNames))
@@ -498,11 +496,10 @@ func (b *Builder) buildInstanceResource(
 
 	// The instance resource has a set of variables that need to be resolved.
 	instance := &Resource{
-		id:             "instance",
-		gvr:            metadata.GVKtoGVR(gvk),
-		schema:         instanceSchema,
-		crd:            instanceCRD,
-		emulatedObject: emulatedInstance,
+		id:     "instance",
+		gvr:    metadata.GVKtoGVR(gvk),
+		schema: instanceSchema,
+		crd:    instanceCRD,
 	}
 
 	instanceStatusVariables := []*variable.ResourceField{}
@@ -569,8 +566,9 @@ func buildInstanceSpecSchema(rgSchema *v1alpha1.Schema) (*extv1.JSONSchemaProps,
 	return instanceSchema, nil
 }
 
-// buildStatusSchema builds the status schema for the instance resource. The
-// status schema is inferred from the CEL expressions in the status field.
+// buildStatusSchema builds the status schema for the instance resource.
+// The status schema is inferred from the CEL expressions in the status field
+// using CEL type checking.
 func buildStatusSchema(
 	rgSchema *v1alpha1.Schema,
 	resources map[string]*Resource,
@@ -586,101 +584,63 @@ func buildStatusSchema(
 		return nil, nil, fmt.Errorf("failed to unmarshal status schema: %w", err)
 	}
 
-	// different from the instance spec, the status schema is inferred from the
-	// CEL expressions in the status field.
+	// Extract CEL expressions from the status field.
 	fieldDescriptors, err := parser.ParseSchemalessResource(unstructuredStatus)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to extract CEL expressions from status: %w", err)
 	}
 
-	// Inspection of the CEL expressions to infer the types of the status fields.
-	resourceNames := maps.Keys(resources)
-
-	env, err := krocel.DefaultEnvironment(krocel.WithResourceIDs(resourceNames))
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to create CEL environment: %w", err)
-	}
-
-	// statusStructureParts := make([]schema.FieldDescriptor, 0, len(extracted))
-	statusDryRunResults := make(map[string][]ref.Val, len(fieldDescriptors))
-	for _, found := range fieldDescriptors {
-		// For each expression in the extracted `ExpressionField` we need to dry-run
-		// the expression to infer the type of the status field.
-		evals := []ref.Val{}
-		for _, expr := range found.Expressions {
-			// we need to inspect the expression to understand how it relates to the
-			// resources defined in the resource graph definition.
-			err := validateCELExpressionContext(env, expr, resourceNames)
-			if err != nil {
-				return nil, nil, fmt.Errorf("failed to validate expression context: %w", err)
-			}
-
-			// resources is the context here.
-			value, err := dryRunExpression(env, expr, resources)
-			if err != nil {
-				return nil, nil, fmt.Errorf("failed to dry-run expression: %w", err)
-			}
-
-			evals = append(evals, value)
+	schemas := make(map[string]*spec.Schema)
+	for id, resource := range resources {
+		if resource.schema != nil {
+			schemas[id] = resource.schema
 		}
-		statusDryRunResults[found.Path] = evals
 	}
 
-	statusSchema, err := schema.GenerateSchemaFromEvals(statusDryRunResults)
+	env, err := krocel.TypedEnvironment(schemas)
 	if err != nil {
-		return nil, nil, fmt.Errorf("failed to build JSON schema from status structure: %w", err)
+		return nil, nil, fmt.Errorf("failed to create typed CEL environment: %w", err)
 	}
-	return statusSchema, fieldDescriptors, nil
-}
 
-// validateCELExpressionContext validates the given CEL expression in the context
-// of the resources defined in the resource graph definition.
-func validateCELExpressionContext(env *cel.Env, expression string, resources []string) error {
-	inspector := ast.NewInspectorWithEnv(env, resources)
+	provider := krocel.CreateDeclTypeProvider(schemas)
 
-	// The CEL expression is valid if it refers to the resources defined in the
-	// resource graph definition.
-	inspectionResult, err := inspector.Inspect(expression)
-	if err != nil {
-		return fmt.Errorf("failed to inspect expression: %w", err)
-	}
-	// make sure that the expression refers to the resources defined in the resource graph definition.
-	for _, resource := range inspectionResult.ResourceDependencies {
-		if !slices.Contains(resources, resource.ID) {
-			return fmt.Errorf("expression refers to unknown resource: %s", resource.ID)
-		}
-	}
-	return nil
-}
+	// Infer types for each status field expression using CEL type checking
+	statusTypeMap := make(map[string]*cel.Type)
+	for _, fieldDescriptor := range fieldDescriptors {
+		if len(fieldDescriptor.Expressions) == 1 {
+			// Single expression - must infer type from expression output
+			expression := fieldDescriptor.Expressions[0]
 
-// dryRunExpression executes the given CEL expression in the context of a set
-// of emulated resources. We could've called this function evaluateExpression,
-// but we chose to call it dryRunExpression to indicate that we are not
-// used for anything other than validating the expression and inspecting it
-func dryRunExpression(env *cel.Env, expression string, resources map[string]*Resource) (ref.Val, error) {
-	ast, issues := env.Compile(expression)
-	if issues != nil && issues.Err() != nil {
-		return nil, fmt.Errorf("failed to compile expression: %w", issues.Err())
-	}
+			checkedAST, err := parseAndCheckCELExpression(env, expression)
+			if err != nil {
+				return nil, nil, fmt.Errorf("failed to type-check status expression %q at path %q: %w", expression, fieldDescriptor.Path, err)
+			}
 
-	// TODO(a-hilaly): thinking about a creating a library to hide this...
-	program, err := env.Program(ast)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create program: %w", err)
-	}
+			statusTypeMap[fieldDescriptor.Path] = checkedAST.OutputType()
+		} else {
+			for _, expression := range fieldDescriptor.Expressions {
+				checkedAST, err := parseAndCheckCELExpression(env, expression)
+				if err != nil {
+					return nil, nil, fmt.Errorf("failed to type-check status expression %q at path %q: %w", expression, fieldDescriptor.Path, err)
+				}
 
-	context := map[string]interface{}{}
-	for resourceName, resource := range resources {
-		if resource.emulatedObject != nil {
-			context[resourceName] = resource.emulatedObject.Object
+				outputType := checkedAST.OutputType()
+				if err := validateExpressionType(outputType, cel.StringType, expression, "status", fieldDescriptor.Path); err != nil {
+					return nil, nil, err
+				}
+			}
+			// All expressions are strings - result type is string
+			statusTypeMap[fieldDescriptor.Path] = cel.StringType
 		}
 	}
 
-	output, _, err := program.Eval(context)
+	// convert the CEL types to OpenAPI schema - best effort.
+	statusSchema, err := schema.GenerateSchemaFromCELTypes(statusTypeMap, provider)
 	if err != nil {
-		return nil, fmt.Errorf("failed to evaluate expression: %w", err)
+		return nil, nil, fmt.Errorf("failed to generate status schema from CEL types: %w", err)
 	}
-	return output, nil
+
+	return statusSchema, fieldDescriptors, nil
 }
 
 // extractDependencies extracts the dependencies from the given CEL expression.
@@ -714,155 +674,188 @@ func extractDependencies(env *cel.Env, expression string, resourceNames []string
 	return dependencies, isStatic, nil
 }
 
-// validateResourceCELExpressions tries to validate the CEL expressions in the
-// resources against the resources defined in the resource graph definition.
-//
-// In this process, we pin a resource and evaluate the CEL expressions in the
-// context of emulated resources. Meaning that given 3 resources A, B, and C,
-// we evaluate A's CEL expressions against 2 emulated resources B and C. Then
-// we evaluate B's CEL expressions against 2 emulated resources A and C, and so
-// on.
-func validateResourceCELExpressions(resources map[string]*Resource, instance *Resource) error {
-	resourceIDs := maps.Keys(resources)
-	// We also want to allow users to refer to the instance spec in their expressions.
-	resourceIDs = append(resourceIDs, "schema")
-
-	env, err := krocel.DefaultEnvironment(krocel.WithResourceIDs(resourceIDs))
-	if err != nil {
-		return fmt.Errorf("failed to create CEL environment: %w", err)
-	}
-	instanceEmulatedCopy := instance.emulatedObject.DeepCopy()
-	if instanceEmulatedCopy != nil && instanceEmulatedCopy.Object != nil {
-		delete(instanceEmulatedCopy.Object, "apiVersion")
-		delete(instanceEmulatedCopy.Object, "kind")
-		delete(instanceEmulatedCopy.Object, "status")
-	}
-
-	// create includeWhenContext
-	includeWhenContext := map[string]*Resource{}
-	// For now, we will only support the instance context for includeWhen expressions.
-	// With this decision, we will decide on creation time and update time
-	// If we'll be creating resources or not
-	includeWhenContext["schema"] = &Resource{
-		emulatedObject: &unstructured.Unstructured{
-			Object: instanceEmulatedCopy.Object,
-		},
-	}
-
-	// create expressionsContext
-	expressionContext := map[string]*Resource{}
-	// add instance spec to the context
-	expressionContext["schema"] = &Resource{
-		emulatedObject: &unstructured.Unstructured{
-			Object: instanceEmulatedCopy.Object,
-		},
+// validateNode validates all CEL expressions for a single resource node:
+// - Template expressions (resource field values)
+// - includeWhen expressions (conditional resource creation)
+// - readyWhen expressions (resource readiness conditions)
+func validateNode(resource *Resource, templatesEnv, schemaEnv *cel.Env, resourceSchema *spec.Schema) error {
+	// Validate template expressions
+	if err := validateTemplateExpressions(templatesEnv, resource); err != nil {
+		return err
 	}
-	// include all resources, and remove individual ones
-	// during the validation
-	// this is done to avoid having to create a new context for each resource
-	for resourceName, contextResource := range resources {
-		expressionContext[resourceName] = contextResource
-	}
-
-	for _, resource := range resources {
-		// exclude resource from the context
-		delete(expressionContext, resource.id)
 
-		err := ensureResourceExpressions(env, expressionContext, resource)
-		if err != nil {
-			return fmt.Errorf("failed to ensure resource %s expressions: %w", resource.id, err)
+	// Validate includeWhen expressions if present
+	if len(resource.includeWhenExpressions) > 0 {
+		if err := validateIncludeWhenExpressions(schemaEnv, resource); err != nil {
+			return err
 		}
+	}
 
-		err = ensureReadyWhenExpressions(resource)
+	// Validate readyWhen expressions if present
+	if len(resource.readyWhenExpressions) > 0 {
+		// Create a CEL environment with only this resource's schema available
+		resourceEnv, err := krocel.TypedEnvironment(map[string]*spec.Schema{resource.id: resourceSchema})
 		if err != nil {
-			return fmt.Errorf("failed to ensure resource %s readyWhen expressions: %w", resource.id, err)
+			return fmt.Errorf("failed to create CEL environment for readyWhen validation: %w", err)
 		}
 
-		err = ensureIncludeWhenExpressions(env, includeWhenContext, resource)
-		if err != nil {
-			return fmt.Errorf("failed to ensure resource %s includeWhen expressions: %w", resource.id, err)
+		if err := validateReadyWhenExpressions(resourceEnv, resource); err != nil {
+			return err
 		}
-
-		// include the resource back to the context
-		expressionContext[resource.id] = resource
 	}
 
 	return nil
 }
 
-// ensureResourceExpressions validates the CEL expressions in the resource
-// against the resources defined in the resource graph definition.
-func ensureResourceExpressions(env *cel.Env, context map[string]*Resource, resource *Resource) error {
-	// We need to validate the CEL expressions in the resource.
-	for _, resourceVariable := range resource.variables {
-		for _, expression := range resourceVariable.Expressions {
-			_, err := ensureExpression(env, expression, []string{resource.id}, context)
+// validateTemplateExpressions validates CEL template expressions for a single resource.
+// It type-checks that expressions reference valid fields and return the expected types
+// based on the OpenAPI schemas.
+func validateTemplateExpressions(env *cel.Env, resource *Resource) error {
+	for _, templateVariable := range resource.variables {
+		if len(templateVariable.Expressions) == 1 {
+			// Single expression - validate against expected types
+			expression := templateVariable.Expressions[0]
+
+			checkedAST, err := parseAndCheckCELExpression(env, expression)
 			if err != nil {
-				return fmt.Errorf("failed to dry-run expression %s: %w", expression, err)
+				return fmt.Errorf("failed to type-check template expression %q at path %q: %w", expression, templateVariable.Path, err)
+			}
+
+			outputType := checkedAST.OutputType()
+			if err := validateExpressionType(outputType, templateVariable.ExpectedType, expression, resource.id, templateVariable.Path); err != nil {
+				return err
+			}
+		} else if len(templateVariable.Expressions) > 1 {
+			// Multiple expressions - all must be strings for concatenation
+			for _, expression := range templateVariable.Expressions {
+				checkedAST, err := parseAndCheckCELExpression(env, expression)
+				if err != nil {
+					return fmt.Errorf("failed to type-check template expression %q at path %q: %w", expression, templateVariable.Path, err)
+				}
+
+				outputType := checkedAST.OutputType()
+				if err := validateExpressionType(outputType, templateVariable.ExpectedType, expression, resource.id, templateVariable.Path); err != nil {
+					return err
+				}
 			}
 		}
 	}
 	return nil
 }
 
-// ensureReadyWhenExpressions validates the readyWhen expressions in the resource
-// against the resources defined in the resource graph definition.
-func ensureReadyWhenExpressions(resource *Resource) error {
-	env, err := krocel.DefaultEnvironment(krocel.WithResourceIDs([]string{resource.id}))
-	for _, expression := range resource.readyWhenExpressions {
-		if err != nil {
-			return fmt.Errorf("failed to create CEL environment: %w", err)
-		}
+// validateExpressionType verifies that the CEL expression output type matches
+// the expected type. Returns an error if there is a type mismatch.
+func validateExpressionType(outputType, expectedType *cel.Type, expression, resourceID, path string) error {
+	if expectedType.IsAssignableType(outputType) {
+		return nil
+	}
 
-		resourceEmulatedCopy := resource.emulatedObject.DeepCopy()
-		if resourceEmulatedCopy != nil && resourceEmulatedCopy.Object != nil {
-			// ignore apiVersion and kind from readyWhenExpression context
-			delete(resourceEmulatedCopy.Object, "apiVersion")
-			delete(resourceEmulatedCopy.Object, "kind")
-		}
-		context := map[string]*Resource{}
-		context[resource.id] = &Resource{
-			emulatedObject: resourceEmulatedCopy,
-		}
+	// Check output is dynamic - always valid
+	if outputType.String() == cel.DynType.String() {
+		return nil
+	}
 
-		output, err := ensureExpression(env, expression, []string{resource.id}, context)
-		if err != nil {
-			return fmt.Errorf("failed to dry-run expression %s: %w", expression, err)
-		}
-		if !krocel.IsBoolType(output) {
-			return fmt.Errorf("output of readyWhen expression %s can only be of type bool", expression)
-		}
+	// Check if unwrapping would fix the type mismatch - provide helpful error message
+	if krocel.WouldMatchIfUnwrapped(outputType, expectedType) {
+		return fmt.Errorf(
+			"type mismatch in resource %q at path %q: expression %q returns %q but field expects %q. "+
+				"Use .orValue(defaultValue) to unwrap the optional type, e.g., %s.orValue(\"\")",
+			resourceID, path, expression, outputType.String(), expectedType.String(), expression,
+		)
+	}
+
+	// Type mismatch - construct helpful error message. This will surface to users.
+	return fmt.Errorf(
+		"type mismatch in resource %q at path %q: expression %q returns type %q but expected %q",
+		resourceID, path, expression, outputType.String(), expectedType.String(),
+	)
+}
+
+// parseAndCheckCELExpression parses and type-checks a CEL expression.
+// Returns the checked AST on success, or the raw CEL error on failure.
+// Callers should wrap the error with appropriate context.
+func parseAndCheckCELExpression(env *cel.Env, expression string) (*cel.Ast, error) {
+	parsedAST, issues := env.Parse(expression)
+	if issues != nil && issues.Err() != nil {
+		return nil, issues.Err()
+	}
+
+	checkedAST, issues := env.Check(parsedAST)
+	if issues != nil && issues.Err() != nil {
+		return nil, issues.Err()
+	}
+
+	return checkedAST, nil
+}
+
+// validateConditionExpression validates a single condition expression (includeWhen or readyWhen).
+// It parses, type-checks, and verifies the expression returns bool or optional_type(bool).
+func validateConditionExpression(env *cel.Env, expression, conditionType, resourceID string) error {
+	checkedAST, err := parseAndCheckCELExpression(env, expression)
+	if err != nil {
+		return fmt.Errorf("failed to type-check %s expression %q in resource %q: %w", conditionType, expression, resourceID, err)
+	}
+
+	// Verify the expression returns bool or optional_type(bool)
+	outputType := checkedAST.OutputType()
+	if !krocel.IsBoolOrOptionalBool(outputType) {
+		return fmt.Errorf(
+			"%s expression %q in resource %q must return bool or optional_type(bool), but returns %q",
+			conditionType, expression, resourceID, outputType.String(),
+		)
 	}
+
 	return nil
 }
 
-// ensureIncludeWhenExpressions validates the includeWhen expressions in the resource
-func ensureIncludeWhenExpressions(env *cel.Env, context map[string]*Resource, resource *Resource) error {
-	// We need to validate the CEL expressions in the resource.
+// validateIncludeWhenExpressions validates that includeWhen expressions:
+// 1. Only reference the "schema" variable
+// 2. Return bool or optional_type(bool)
+// validateIncludeWhenExpressions validates includeWhen expressions for a single resource.
+func validateIncludeWhenExpressions(env *cel.Env, resource *Resource) error {
 	for _, expression := range resource.includeWhenExpressions {
-		output, err := ensureExpression(env, expression, []string{resource.id}, context)
-		if err != nil {
-			return fmt.Errorf("failed to dry-run expression %s: %w", expression, err)
+		if err := validateConditionExpression(env, expression, "includeWhen", resource.id); err != nil {
+			return err
 		}
-		if !krocel.IsBoolType(output) {
-			return fmt.Errorf("output of includeWhen expression %s can only be of type bool", expression)
+	}
+	return nil
+}
+
+// validateReadyWhenExpressions validates readyWhen expressions for a single resource.
+func validateReadyWhenExpressions(env *cel.Env, resource *Resource) error {
+	for _, expression := range resource.readyWhenExpressions {
+		if err := validateConditionExpression(env, expression, "readyWhen", resource.id); err != nil {
+			return err
 		}
 	}
 	return nil
 }
 
-// ensureExpression validates the CEL expression in the context of the resources
-func ensureExpression(env *cel.Env, expression string, resources []string, context map[string]*Resource) (ref.Val, error) {
-	err := validateCELExpressionContext(env, expression, resources)
-	if err != nil {
-		return nil, fmt.Errorf("failed to validate expression %s: %w", expression, err)
+// getSchemaWithoutStatus returns a schema from the CRD with the status field removed.
+func getSchemaWithoutStatus(crd *extv1.CustomResourceDefinition) (*spec.Schema, error) {
+	crdCopy := crd.DeepCopy()
+
+	// TODO(a-hilaly) expand this function when we start support CRD upgrades.
+	if len(crdCopy.Spec.Versions) != 1 || crdCopy.Spec.Versions[0].Schema == nil {
+		panic("Expected CRD to have exactly one version with schema defined")
 	}
 
-	output, err := dryRunExpression(env, expression, context)
-	if err != nil {
-		return nil, fmt.Errorf("failed to dry-run expression %s: %w", expression, err)
+	openAPISchema := crdCopy.Spec.Versions[0].Schema.OpenAPIV3Schema
+
+	if openAPISchema.Properties == nil {
+		openAPISchema.Properties = make(map[string]extv1.JSONSchemaProps)
 	}
 
-	return output, nil
+	delete(openAPISchema.Properties, "status")
 
+	specSchema, err := schema.ConvertJSONSchemaPropsToSpecSchema(openAPISchema)
+	if err != nil {
+		return nil, err
+	}
+
+	if specSchema.Properties == nil {
+		specSchema.Properties = make(map[string]spec.Schema)
+	}
+	specSchema.Properties["metadata"] = schema.ObjectMetaSchema
+	return specSchema, nil
 }
diff --git a/pkg/graph/parser/conditions.go b/pkg/graph/parser/conditions.go
index 42303bf..9b56b98 100644
--- a/pkg/graph/parser/conditions.go
+++ b/pkg/graph/parser/conditions.go
@@ -38,7 +38,9 @@ func ParseConditionExpressions(conditions []string) ([]string, error) {
 		if !ok {
 			return nil, fmt.Errorf("only standalone expressions are allowed")
 		}
-		expressions = append(expressions, strings.Trim(e, "${}"))
+		expr := strings.TrimPrefix(e, "${")
+		expr = strings.TrimSuffix(expr, "}")
+		expressions = append(expressions, expr)
 	}
 
 	return expressions, nil
diff --git a/pkg/graph/parser/parser.go b/pkg/graph/parser/parser.go
index acf9b42..aa7aa0c 100644
--- a/pkg/graph/parser/parser.go
+++ b/pkg/graph/parser/parser.go
@@ -19,6 +19,8 @@ import (
 	"slices"
 	"strings"
 
+	"github.com/google/cel-go/cel"
+	"k8s.io/apiserver/pkg/cel/openapi"
 	"k8s.io/kube-openapi/pkg/validation/spec"
 
 	"github.com/kubernetes-sigs/kro/pkg/graph/variable"
@@ -42,7 +44,7 @@ func ParseResource(resource map[string]interface{}, resourceSchema *spec.Schema)
 }
 
 // parseResource is a helper function that recursively extracts CEL expressions
-// from a resource. It uses a depthh first search to traverse the resource and
+// from a resource. It uses a depth first search to traverse the resource and
 // extract expressions from string fields
 func parseResource(resource interface{}, schema *spec.Schema, path string) ([]variable.FieldDescriptor, error) {
 	if err := validateSchema(schema, path); err != nil {
@@ -68,6 +70,24 @@ func parseResource(resource interface{}, schema *spec.Schema, path string) ([]va
 	}
 }
 
+// getCelType converts an OpenAPI schema to a CEL type using the Kubernetes OpenAPI library.
+// This replaces manual type conversion with the library's schema-to-CEL type conversion.
+func getCelType(schema *spec.Schema) *cel.Type {
+	if schema == nil {
+		return cel.DynType
+	}
+
+	// Use the Kubernetes OpenAPI library to convert schema to CEL type
+	declType := openapi.SchemaDeclType(schema, false)
+	if declType == nil {
+		return cel.DynType
+	}
+
+	return declType.CelType()
+}
+
+// getExpectedTypes extracts the expected types from a schema for validation purposes.
+// This is used for non-CEL values to ensure proper type validation.
 func getExpectedTypes(schema *spec.Schema) ([]string, error) {
 	// Handle extensions (like x-kubernetes-int-or-string)
 	if types, found := handleSchemaExtensions(schema); found {
@@ -88,8 +108,7 @@ func getExpectedTypes(schema *spec.Schema) ([]string, error) {
 	if schema.AdditionalProperties != nil && schema.AdditionalProperties.Allows {
 		// NOTE(a-hilaly): I don't like the type "any", we might want to change this to "object"
 		// in the future; just haven't really thought about it yet.
-		// Basically "any" means that the field can be of any type, and we have to check
-		// the ExpectedSchema field.
+		// Basically "any" means that the field can be of any type.
 		return []string{schemaTypeAny}, nil
 	}
 
@@ -257,10 +276,13 @@ func parseString(field string, schema *spec.Schema, path string, expectedTypes [
 	}
 
 	if ok {
+		// For CEL expressions, get the CEL type from the schema
+		expectedType := getCelType(schema)
+		expr := strings.TrimPrefix(field, "${")
+		expr = strings.TrimSuffix(expr, "}")
 		return []variable.FieldDescriptor{{
-			Expressions:          []string{strings.Trim(field, "${}")},
-			ExpectedTypes:        expectedTypes,
-			ExpectedSchema:       schema,
+			Expressions:          []string{expr},
+			ExpectedType:         expectedType,
 			Path:                 path,
 			StandaloneExpression: true,
 		}}, nil
@@ -275,36 +297,56 @@ func parseString(field string, schema *spec.Schema, path string, expectedTypes [
 		return nil, err
 	}
 	if len(expressions) > 0 {
+		// String templates always produce strings
 		return []variable.FieldDescriptor{{
-			Expressions:   expressions,
-			ExpectedTypes: expectedTypes,
-			Path:          path,
+			Expressions:  expressions,
+			ExpectedType: cel.StringType,
+			Path:         path,
 		}}, nil
 	}
 	return nil, nil
 }
 
 func parseScalarTypes(field interface{}, _ *spec.Schema, path string, expectedTypes []string) ([]variable.FieldDescriptor, error) {
-	// perform type checks for scalar types
-	switch {
-	case slices.Contains(expectedTypes, "any"):
-		// Skip checks for expected == provided type
-	case slices.Contains(expectedTypes, "number"):
-		if !isNumber(field) {
-			return nil, fmt.Errorf("expected number type for path %s, got %T", path, field)
-		}
-	case slices.Contains(expectedTypes, "int"), slices.Contains(expectedTypes, "integer"):
-		if !isInteger(field) {
-			return nil, fmt.Errorf("expected integer type for path %s, got %T", path, field)
-		}
-	case slices.Contains(expectedTypes, "boolean"), slices.Contains(expectedTypes, "bool"):
-		if _, ok := field.(bool); !ok {
-			return nil, fmt.Errorf("expected boolean type for path %s, got %T", path, field)
+	// If "any" type is expected, skip validation
+	if slices.Contains(expectedTypes, "any") {
+		return nil, nil
+	}
+
+	// Check if the value matches any of the expected types
+	actualType := getSchemaTypeName(field)
+	for _, expected := range expectedTypes {
+		switch expected {
+		case "number":
+			if isNumber(field) {
+				return nil, nil
+			}
+		case "int", "integer":
+			if isInteger(field) {
+				return nil, nil
+			}
+		case "boolean", "bool":
+			if _, ok := field.(bool); ok {
+				return nil, nil
+			}
 		}
+	}
+
+	// No match found - return error with all expected types
+	return nil, fmt.Errorf("expected %s type for path %s, got %s", strings.Join(expectedTypes, " or "), path, actualType)
+}
+
+// getSchemaTypeName converts a Go type to its OpenAPI schema type name
+func getSchemaTypeName(v interface{}) string {
+	switch v.(type) {
+	case bool:
+		return "boolean"
+	case int, int8, int16, int32, int64:
+		return "integer"
 	default:
-		return nil, fmt.Errorf("unexpected type for path %s: %T, expectedTypes: [%s]", path, field, strings.Join(expectedTypes, ","))
+		// For other types (including float), use the Go type name
+		return fmt.Sprintf("%T", v)
 	}
-	return nil, nil
 }
 
 func getFieldSchema(schema *spec.Schema, field string) (*spec.Schema, error) {
diff --git a/pkg/graph/parser/schemaless.go b/pkg/graph/parser/schemaless.go
index 1e810a1..4ad9a9c 100644
--- a/pkg/graph/parser/schemaless.go
+++ b/pkg/graph/parser/schemaless.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/google/cel-go/cel"
 	"github.com/kubernetes-sigs/kro/pkg/graph/variable"
 )
 
@@ -57,9 +58,11 @@ func parseSchemalessResource(resource interface{}, path string) ([]variable.Fiel
 			return nil, err
 		}
 		if ok {
+			expr := strings.TrimPrefix(field, "${")
+			expr = strings.TrimSuffix(expr, "}")
 			expressionsFields = append(expressionsFields, variable.FieldDescriptor{
-				Expressions:          []string{strings.Trim(field, "${}")},
-				ExpectedTypes:        []string{"any"},
+				Expressions:          []string{expr},
+				ExpectedType:         cel.DynType, // No schema, so we use dynamic type
 				Path:                 path,
 				StandaloneExpression: true,
 			})
@@ -70,9 +73,9 @@ func parseSchemalessResource(resource interface{}, path string) ([]variable.Fiel
 			}
 			if len(expressions) > 0 {
 				expressionsFields = append(expressionsFields, variable.FieldDescriptor{
-					Expressions:   expressions,
-					ExpectedTypes: []string{"any"},
-					Path:          path,
+					Expressions:  expressions,
+					ExpectedType: cel.StringType, // String templates always produce strings
+					Path:         path,
 				})
 			}
 		}
diff --git a/pkg/graph/resource.go b/pkg/graph/resource.go
index b227b0f..f2914d7 100644
--- a/pkg/graph/resource.go
+++ b/pkg/graph/resource.go
@@ -47,15 +47,6 @@ type Resource struct {
 	// This will contain all fields (and CEL expressions) as they were in the
 	// original object.
 	originalObject *unstructured.Unstructured
-	// emulatedObject is the object we'll use to emulate the resource during
-	// the graph building process. This object will contain the resolved values
-	// of the CEL expressions.
-	//
-	// NOTE(a-hilaly): Do we need to keep this object? we only need it when
-	// we're building the graph. We can remove it after the graph is built.
-	// Or... maybe we can keep a global cache to reduce the effort of rebuilding
-	// the graph every time.
-	emulatedObject *unstructured.Unstructured
 	// variables is a list of the variables found in the resource (CEL expressions).
 	variables []*variable.ResourceField
 	// dependencies is a list of the resources this resource depends on.
@@ -141,11 +132,6 @@ func (r *Resource) GetSchema() *spec.Schema {
 	return r.schema
 }
 
-// GetEmulatedObject returns the emulated object of the resource.
-func (r *Resource) GetEmulatedObject() *unstructured.Unstructured {
-	return r.emulatedObject
-}
-
 // GetReadyWhenExpressions returns the readyWhen expressions of the resource.
 func (r *Resource) GetReadyWhenExpressions() []string {
 	return r.readyWhenExpressions
diff --git a/pkg/graph/schema/conversion_cel_type.go b/pkg/graph/schema/conversion_cel_type.go
new file mode 100644
index 0000000..8d91d27
--- /dev/null
+++ b/pkg/graph/schema/conversion_cel_type.go
@@ -0,0 +1,253 @@
+// Copyright 2025 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package schema
+
+import (
+	"fmt"
+
+	"github.com/google/cel-go/cel"
+	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	apiservercel "k8s.io/apiserver/pkg/cel"
+	"k8s.io/utils/ptr"
+)
+
+// GenerateSchemaFromCELTypes generates a JSONSchemaProps from a map of CEL types.
+// The provider is used to recursively introspect struct types and extract all fields.
+func GenerateSchemaFromCELTypes(typeMap map[string]*cel.Type, provider *apiservercel.DeclTypeProvider) (*extv1.JSONSchemaProps, error) {
+	fieldDescriptors := make([]fieldDescriptor, 0, len(typeMap))
+
+	for path, celType := range typeMap {
+		exprSchema, err := inferSchemaFromCELType(celType, provider)
+		if err != nil {
+			return nil, fmt.Errorf("failed to infer schema from type at %v: %w", path, err)
+		}
+		fieldDescriptors = append(fieldDescriptors, fieldDescriptor{
+			Path:   path,
+			Schema: exprSchema,
+		})
+	}
+
+	return generateJSONSchemaFromFieldDescriptors(fieldDescriptors)
+}
+
+// primitiveTypeToSchema converts a CEL primitive type name to a JSONSchemaProps.
+func primitiveTypeToSchema(typeName string) (*extv1.JSONSchemaProps, bool) {
+	switch typeName {
+	case "bool":
+		return &extv1.JSONSchemaProps{Type: "boolean"}, true
+	case "int", "uint":
+		return &extv1.JSONSchemaProps{Type: "integer"}, true
+	case "double":
+		return &extv1.JSONSchemaProps{Type: "number"}, true
+	case "string":
+		return &extv1.JSONSchemaProps{Type: "string"}, true
+	case "bytes":
+		return &extv1.JSONSchemaProps{Type: "string", Format: "byte"}, true
+	case "null_type":
+		return &extv1.JSONSchemaProps{
+			Description:            "null type - any value allowed",
+			XPreserveUnknownFields: ptr.To(true),
+		}, true
+	default:
+		return nil, false
+	}
+}
+
+// inferSchemaFromCELType converts a CEL type to an OpenAPI schema.
+// For struct types, it uses the provider to recursively extract all fields.
+func inferSchemaFromCELType(celType *cel.Type, provider *apiservercel.DeclTypeProvider) (*extv1.JSONSchemaProps, error) {
+	if celType == nil {
+		return nil, fmt.Errorf("type is nil")
+	}
+
+	typeName := celType.String()
+
+	// Handle primitive types
+	if schema, ok := primitiveTypeToSchema(typeName); ok {
+		return schema, nil
+	}
+
+	// Handle complex types based on kind
+	switch celType.Kind() {
+	case cel.ListKind:
+		if provider != nil {
+			declType, found := provider.FindDeclType(typeName)
+			if found {
+				visited := make(map[string]bool)
+				return extractSchemaFromDeclTypeWithCycleDetection(declType, visited)
+			}
+		}
+		// Fallback: try to infer from CEL type parameters
+		if len(celType.Parameters()) > 0 {
+			elemType := celType.Parameters()[0]
+			elemSchema, err := inferSchemaFromCELType(elemType, provider)
+			if err != nil {
+				return nil, fmt.Errorf("failed to infer array element type: %w", err)
+			}
+			return &extv1.JSONSchemaProps{
+				Type: "array",
+				Items: &extv1.JSONSchemaPropsOrArray{
+					Schema: elemSchema,
+				},
+			}, nil
+		}
+		return &extv1.JSONSchemaProps{Type: "array"}, nil
+
+	case cel.MapKind:
+		if provider != nil {
+			declType, found := provider.FindDeclType(typeName)
+			if found {
+				visited := make(map[string]bool)
+				return extractSchemaFromDeclTypeWithCycleDetection(declType, visited)
+			}
+		}
+		// Fallback: try to infer from CEL type parameters
+		if len(celType.Parameters()) > 1 {
+			valueType := celType.Parameters()[1]
+			valueSchema, err := inferSchemaFromCELType(valueType, provider)
+			if err != nil {
+				return nil, fmt.Errorf("failed to infer map value type: %w", err)
+			}
+			return &extv1.JSONSchemaProps{
+				Type: "object",
+				AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
+					Schema: valueSchema,
+				},
+			}, nil
+		}
+		return &extv1.JSONSchemaProps{
+			Type:                 "object",
+			AdditionalProperties: &extv1.JSONSchemaPropsOrBool{Allows: true},
+		}, nil
+
+	case cel.StructKind:
+		if provider != nil {
+			declType, found := provider.FindDeclType(typeName)
+			if found {
+				visited := make(map[string]bool)
+				return extractSchemaFromDeclTypeWithCycleDetection(declType, visited)
+			}
+		}
+		// Fallback: permissive object schema
+		return &extv1.JSONSchemaProps{
+			Type:                 "object",
+			AdditionalProperties: &extv1.JSONSchemaPropsOrBool{Allows: true},
+		}, nil
+
+	case cel.DynKind:
+		return &extv1.JSONSchemaProps{
+			XPreserveUnknownFields: ptr.To(true),
+		}, nil
+
+	default:
+		// Unknown type - be permissive
+		return &extv1.JSONSchemaProps{
+			Description:            fmt.Sprintf("unknown CEL type: %s", typeName),
+			XPreserveUnknownFields: ptr.To(true),
+		}, nil
+	}
+}
+
+// extractSchemaFromDeclTypeWithCycleDetection recursively extracts all fields from a DeclType to build an OpenAPI schema.
+// It tracks visited types to prevent infinite recursion on cyclic type definitions.
+func extractSchemaFromDeclTypeWithCycleDetection(declType *apiservercel.DeclType, visited map[string]bool) (*extv1.JSONSchemaProps, error) {
+	if declType == nil {
+		return &extv1.JSONSchemaProps{XPreserveUnknownFields: ptr.To(true)}, nil
+	}
+
+	// Get type identifier for cycle detection
+	celType := declType.CelType()
+	if celType != nil {
+		typeName := celType.String()
+
+		if visited[typeName] {
+			return nil, fmt.Errorf("cyclic type reference detected: %s", typeName)
+		}
+
+		visited[typeName] = true
+		defer delete(visited, typeName)
+	}
+
+	// Handle different DeclType kinds
+	if declType.IsList() {
+		elemType := declType.ElemType
+		if elemType != nil {
+			elemSchema, err := extractSchemaFromDeclTypeWithCycleDetection(elemType, visited)
+			if err != nil {
+				return nil, fmt.Errorf("failed to extract array element schema: %w", err)
+			}
+			return &extv1.JSONSchemaProps{
+				Type: "array",
+				Items: &extv1.JSONSchemaPropsOrArray{
+					Schema: elemSchema,
+				},
+			}, nil
+		}
+		return &extv1.JSONSchemaProps{Type: "array"}, nil
+	}
+
+	if declType.IsMap() {
+		valueType := declType.ElemType
+		if valueType != nil {
+			valueSchema, err := extractSchemaFromDeclTypeWithCycleDetection(valueType, visited)
+			if err != nil {
+				return nil, fmt.Errorf("failed to extract map value schema: %w", err)
+			}
+			return &extv1.JSONSchemaProps{
+				Type: "object",
+				AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
+					Schema: valueSchema,
+				},
+			}, nil
+		}
+		return &extv1.JSONSchemaProps{
+			Type:                 "object",
+			AdditionalProperties: &extv1.JSONSchemaPropsOrBool{Allows: true},
+		}, nil
+	}
+
+	if !declType.IsObject() {
+		celType := declType.CelType()
+		if celType != nil {
+			typeName := celType.String()
+			if schema, ok := primitiveTypeToSchema(typeName); ok {
+				return schema, nil
+			}
+		}
+		return &extv1.JSONSchemaProps{XPreserveUnknownFields: ptr.To(true)}, nil
+	}
+
+	schema := &extv1.JSONSchemaProps{
+		Type:       "object",
+		Properties: make(map[string]extv1.JSONSchemaProps),
+	}
+
+	for _, field := range declType.Fields {
+		if field == nil {
+			continue
+		}
+
+		fieldSchema, err := extractSchemaFromDeclTypeWithCycleDetection(field.Type, visited)
+		if err != nil {
+			return nil, fmt.Errorf("failed to extract schema for field %s: %w", field.Name, err)
+		}
+
+		if fieldSchema != nil {
+			schema.Properties[field.Name] = *fieldSchema
+		}
+	}
+
+	return schema, nil
+}
diff --git a/pkg/graph/schema/schema.go b/pkg/graph/schema/schema.go
new file mode 100644
index 0000000..90da98e
--- /dev/null
+++ b/pkg/graph/schema/schema.go
@@ -0,0 +1,58 @@
+// Copyright 2025 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package schema
+
+import (
+	"fmt"
+
+	"k8s.io/apiextensions-apiserver/pkg/generated/openapi"
+	"k8s.io/apiserver/pkg/cel/openapi/resolver"
+	"k8s.io/kube-openapi/pkg/validation/spec"
+)
+
+// ObjectMeta holds the k8s ObjectMeta schema, populated once at startup.
+var ObjectMetaSchema spec.Schema
+
+func init() {
+	// Populate ObjectMeta schema once at startup to avoid repeated query operations.
+	var err error
+	ObjectMetaSchema, err = getObjectMetaSchema()
+	if err != nil {
+		// This should never happen as getObjectMetaSchemaUncached only fails if
+		// Kubernetes OpenAPI definitions are missing, which would be a
+		// critical build/dependency issue.
+		panic(fmt.Sprintf("failed to initialize ObjectMeta schema: %v", err))
+	}
+}
+
+// getObjectMetaSchema extracts the ObjectMeta schema from Kubernetes OpenAPI definitions.
+// This returns the fully resolved ObjectMeta schema including all nested types like
+// OwnerReference, ManagedFieldsEntry, Time, etc.
+func getObjectMetaSchema() (spec.Schema, error) {
+	// get OpenAPI definitions from apiserver package
+	definitions := openapi.GetOpenAPIDefinitions(spec.MustCreateRef)
+	populatedSchema, err := resolver.PopulateRefs(func(ref string) (*spec.Schema, bool) {
+		def, ok := definitions[ref]
+		if !ok {
+			return nil, false
+		}
+		s := def.Schema
+		return &s, true
+	}, "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta")
+	if err != nil {
+		return spec.Schema{}, fmt.Errorf("failed to populate refs for ObjectMeta: %w", err)
+	}
+	return *populatedSchema, nil
+}
diff --git a/pkg/graph/variable/variable.go b/pkg/graph/variable/variable.go
index c67ba1b..99aec7f 100644
--- a/pkg/graph/variable/variable.go
+++ b/pkg/graph/variable/variable.go
@@ -17,7 +17,7 @@ package variable
 import (
 	"slices"
 
-	"k8s.io/kube-openapi/pkg/validation/spec"
+	"github.com/google/cel-go/cel"
 )
 
 // FieldDescriptor represents a field that contains CEL expressions in it. It
@@ -32,12 +32,10 @@ type FieldDescriptor struct {
 	Path string
 	// Expressions is a list of CEL expressions in the field.
 	Expressions []string
-	// ExpectedType is the expected type of the field.
-	ExpectedTypes []string
-	// ExpectedSchema is the expected schema of the field if it is a complex type.
-	// This is only set if the field is a OneShotCEL expression, and the schema
-	// is expected to be a complex type (object or array).
-	ExpectedSchema *spec.Schema
+	// ExpectedType is the expected CEL type of the field, derived from the OpenAPI schema.
+	// For single expressions, this is the schema's CEL type.
+	// For multiple expressions (string templates), this is always cel.StringType.
+	ExpectedType *cel.Type
 	// StandaloneExpression is true if the field contains a single CEL expression
 	// that is not part of a larger string. example: "${foo}" is a standalone expression
 	// but not "hello-${foo}" or "${foo}${bar}"
diff --git a/pkg/runtime/resolver/resolver.go b/pkg/runtime/resolver/resolver.go
index b733231..ebcfd1d 100644
--- a/pkg/runtime/resolver/resolver.go
+++ b/pkg/runtime/resolver/resolver.go
@@ -104,7 +104,7 @@ func (r *Resolver) resolveField(field variable.FieldDescriptor) ResolutionResult
 	}
 
 	if field.StandaloneExpression {
-		resolvedValue, ok := r.data[strings.Trim(field.Expressions[0], "${}")]
+		resolvedValue, ok := r.data[field.Expressions[0]]
 		if !ok {
 			result.Error = fmt.Errorf("no data provided for expression: %s", field.Expressions[0])
 			return result
@@ -125,8 +125,7 @@ func (r *Resolver) resolveField(field variable.FieldDescriptor) ResolutionResult
 
 		replaced := strValue
 		for _, expr := range field.Expressions {
-			key := strings.Trim(expr, "${}")
-			replacement, ok := r.data[key]
+			replacement, ok := r.data[expr]
 			if !ok {
 				result.Error = fmt.Errorf("no data provided for expression: %s", expr)
 				return result
diff --git a/website/docs/examples/aws/pod-rds-dbinstance.md b/website/docs/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/docs/examples/aws/pod-rds-dbinstance.md
+++ b/website/docs/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.1.0/examples/pod-rds-dbinstance.md b/website/versioned_docs/version-0.1.0/examples/pod-rds-dbinstance.md
index 3cc93e7..deb01ec 100644
--- a/website/versioned_docs/version-0.1.0/examples/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.1.0/examples/pod-rds-dbinstance.md
@@ -46,6 +46,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.2.0/examples/pod-rds-dbinstance.md b/website/versioned_docs/version-0.2.0/examples/pod-rds-dbinstance.md
index 9fba4b4..a765a24 100644
--- a/website/versioned_docs/version-0.2.0/examples/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.2.0/examples/pod-rds-dbinstance.md
@@ -46,6 +46,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.2.1/examples/aws/pod-rds-dbinstance.md b/website/versioned_docs/version-0.2.1/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/versioned_docs/version-0.2.1/examples/aws/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.2.1/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.2.2/examples/aws/pod-rds-dbinstance.md b/website/versioned_docs/version-0.2.2/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/versioned_docs/version-0.2.2/examples/aws/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.2.2/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.3.0/examples/aws/pod-rds-dbinstance.md b/website/versioned_docs/version-0.3.0/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/versioned_docs/version-0.3.0/examples/aws/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.3.0/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.4.0/examples/aws/pod-rds-dbinstance.md b/website/versioned_docs/version-0.4.0/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/versioned_docs/version-0.4.0/examples/aws/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.4.0/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.4.1/examples/aws/pod-rds-dbinstance.md b/website/versioned_docs/version-0.4.1/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/versioned_docs/version-0.4.1/examples/aws/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.4.1/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.5.0/examples/aws/pod-rds-dbinstance.md b/website/versioned_docs/version-0.5.0/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/versioned_docs/version-0.5.0/examples/aws/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.5.0/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```
diff --git a/website/versioned_docs/version-0.5.1/examples/aws/pod-rds-dbinstance.md b/website/versioned_docs/version-0.5.1/examples/aws/pod-rds-dbinstance.md
index 3b06711..9f0ca72 100644
--- a/website/versioned_docs/version-0.5.1/examples/aws/pod-rds-dbinstance.md
+++ b/website/versioned_docs/version-0.5.1/examples/aws/pod-rds-dbinstance.md
@@ -45,6 +45,6 @@ spec:
             - name: container1
               image: ${schema.spec.image}
               env:
-                - name: POSTGRESS_ENDPOINT
+                - name: POSTGRES_ENDPOINT
                   value: ${dbinstance.status.endpoint.address}
 ```



Test patch:
diff --git a/pkg/graph/emulator/emulator_test.go b/cmd/kro/emulator/emulator_test.go
similarity index 100%
rename from pkg/graph/emulator/emulator_test.go
rename to cmd/kro/emulator/emulator_test.go
diff --git a/pkg/dynamiccontroller/dynamic_controller_test.go b/pkg/dynamiccontroller/dynamic_controller_test.go
index af3b53b..60e3017 100644
--- a/pkg/dynamiccontroller/dynamic_controller_test.go
+++ b/pkg/dynamiccontroller/dynamic_controller_test.go
@@ -18,19 +18,25 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"maps"
-	"slices"
+	"sync"
 	"testing"
 	"time"
 
 	"github.com/go-logr/logr"
+	"github.com/kubernetes-sigs/kro/pkg/metadata"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/dynamic/fake"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/metadata/fake"
+	k8stesting "k8s.io/client-go/testing"
 	controllerruntime "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 )
 
@@ -48,20 +54,170 @@ func noopLogger() logr.Logger {
 	return logger
 }
 
-func setupFakeClient() *fake.FakeDynamicClient {
+func setupFakeClient(t testing.TB) (*fake.FakeMetadataClient, meta.RESTMapper) {
+	t.Helper()
 	scheme := runtime.NewScheme()
-	gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"}
+	assert.NoError(t, v1.AddMetaToScheme(scheme))
 	gvk := schema.GroupVersionKind{Group: "test", Version: "v1", Kind: "Test"}
-	obj := &unstructured.Unstructured{}
+	obj := &v1.PartialObjectMetadata{}
 	obj.SetGroupVersionKind(gvk)
-	return fake.NewSimpleDynamicClientWithCustomListKinds(scheme, map[schema.GroupVersionResource]string{
-		gvr: "TestList",
-	}, obj)
+	return fake.NewSimpleMetadataClient(scheme, obj), meta.NewDefaultRESTMapper(scheme.PreferredVersionAllGroups())
+}
+
+func TestDynamicController_WatchBehavior(t *testing.T) {
+	deploy := &appsv1.Deployment{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "test-configmap",
+			Namespace: "default",
+		},
+	}
+
+	secret := &corev1.Secret{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "test-secret",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"test": []byte("bar"),
+		},
+	}
+
+	scheme := runtime.NewScheme()
+	require.NoError(t, corev1.AddToScheme(scheme))
+	require.NoError(t, appsv1.AddToScheme(scheme))
+	require.NoError(t, v1.AddMetaToScheme(scheme))
+
+	pdeploy := &v1.PartialObjectMetadata{}
+	pdeploy.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"})
+	pdeploy.SetName(deploy.Name)
+	pdeploy.SetNamespace(deploy.Namespace)
+
+	psecret := &v1.PartialObjectMetadata{}
+	psecret.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: "Secret"})
+	psecret.SetName(secret.Name)
+	psecret.SetNamespace(secret.Namespace)
+
+	client := fake.NewSimpleMetadataClient(scheme, pdeploy, psecret)
+	deploymentUpdates := make(chan watch.Event, 10)
+	client.PrependWatchReactor("deployments", func(action k8stesting.Action) (handled bool, ret watch.Interface, err error) {
+		return true, watch.NewProxyWatcher(deploymentUpdates), nil
+	})
+	secretUpdates := make(chan watch.Event, 10)
+	client.PrependWatchReactor("secrets", func(action k8stesting.Action) (handled bool, ret watch.Interface, err error) {
+		return true, watch.NewProxyWatcher(secretUpdates), nil
+	})
+
+	mapper := meta.NewDefaultRESTMapper(scheme.PreferredVersionAllGroups())
+	deployGVK, err := apiutil.GVKForObject(deploy, scheme)
+	require.NoError(t, err)
+	mapper.Add(deployGVK, meta.RESTScopeNamespace)
+	deployRESTMapping, err := mapper.RESTMapping(deployGVK.GroupKind(), deployGVK.Version)
+	require.NoError(t, err)
+	deployGVR := deployRESTMapping.Resource
+
+	secretGVK, err := apiutil.GVKForObject(secret, scheme)
+	require.NoError(t, err)
+	mapper.Add(secretGVK, meta.RESTScopeNamespace)
+	secretRESTMapping, err := mapper.RESTMapping(secretGVK.GroupKind(), secretGVK.Version)
+	require.NoError(t, err)
+	secretGVR := secretRESTMapping.Resource
+
+	ctrl := NewDynamicController(noopLogger(), Config{
+		Workers:              1,
+		ResyncPeriod:         1 * time.Hour,
+		QueueMaxRetries:      5,
+		MinRetryDelay:        100 * time.Millisecond,
+		MaxRetryDelay:        1 * time.Second,
+		RateLimit:            10,
+		BurstLimit:           100,
+		QueueShutdownTimeout: 5 * time.Second,
+	}, client, mapper)
+
+	var mu sync.Mutex
+	reconciled := make(map[string]int)
+
+	handler := Handler(func(ctx context.Context, req controllerruntime.Request) error {
+		mu.Lock()
+		defer mu.Unlock()
+		reconciled[req.Namespace+"/"+req.Name]++
+		return nil
+	})
+
+	ctx, cancel := context.WithCancel(t.Context())
+	defer cancel()
+
+	go func() {
+		if err := ctrl.Start(ctx); err != nil {
+			require.ErrorIs(t, err, context.Canceled)
+		}
+	}()
+	require.Eventually(t, func() bool {
+		return ctrl.ctx != nil
+	}, 5*time.Second, 100*time.Millisecond)
+
+	// Register parent (ConfigMap) watching child (Secret)
+	require.NoError(t, ctrl.Register(ctx, deployGVR, handler, secretGVR))
+
+	// Simulate Secret update triggering ConfigMap reconciliation
+	// first propagate a modification (like adding a finalizer, without adding any ownership)
+	psecret.SetFinalizers(append(psecret.GetFinalizers(), "test"))
+	secretUpdates <- watch.Event{
+		Type:   watch.Modified,
+		Object: psecret.DeepCopy(),
+	}
+	psecret.SetLabels(map[string]string{
+		metadata.OwnedLabel:             "true",
+		metadata.InstanceLabel:          deploy.GetName(),
+		metadata.InstanceNamespaceLabel: deploy.GetNamespace(),
+	})
+	secretUpdates <- watch.Event{
+		Type:   watch.Modified,
+		Object: psecret.DeepCopy(),
+	}
+
+	// Wait for initial reconciliation of parent
+	require.Eventually(t, func() bool {
+		mu.Lock()
+		defer mu.Unlock()
+		return reconciled[fmt.Sprintf("%s/%s", deploy.GetNamespace(), deploy.GetName())] == 1
+	}, 5*time.Second, 100*time.Millisecond)
+
+	pdeploy = pdeploy.DeepCopy()
+	pdeploy.Labels = map[string]string{
+		"some-label": "some-value",
+	}
+	pdeploy.SetGeneration(deploy.GetGeneration() + 1)
+	deploymentUpdates <- watch.Event{
+		Type:   watch.Modified,
+		Object: pdeploy.DeepCopy(),
+	}
+	// Wait for parent to reconcile again due to parent generation change
+	require.Eventually(t, func() bool {
+		mu.Lock()
+		defer mu.Unlock()
+		return reconciled[fmt.Sprintf("%s/%s", deploy.GetNamespace(), deploy.GetName())] == 2
+	}, 5*time.Second, 100*time.Millisecond)
+
+	mu.Lock()
+	defer mu.Unlock()
+	assert.GreaterOrEqual(t, reconciled["default/test-configmap"], 2)
+
+	_, registrationExists := ctrl.registrations[deployGVR]
+	assert.True(t, registrationExists)
+	_, watchExists := ctrl.watches[deployGVR]
+	assert.True(t, watchExists)
+
+	// Deregister and verify cleanup
+	require.NoError(t, ctrl.Deregister(ctx, deployGVR))
+	_, registrationExists = ctrl.registrations[deployGVR]
+	assert.False(t, registrationExists)
+	_, watchExists = ctrl.watches[deployGVR]
+	assert.False(t, watchExists)
 }
 
 func TestNewDynamicController(t *testing.T) {
 	logger := noopLogger()
-	client := setupFakeClient()
+	client, mapper := setupFakeClient(t)
 
 	config := Config{
 		Workers:         2,
@@ -73,17 +229,16 @@ func TestNewDynamicController(t *testing.T) {
 		BurstLimit:      100,
 	}
 
-	dc := NewDynamicController(logger, config, client)
+	dc := NewDynamicController(logger, config, client, mapper)
 
 	assert.NotNil(t, dc)
 	assert.Equal(t, config, dc.config)
 	assert.NotNil(t, dc.queue)
-	assert.NotNil(t, dc.kubeClient)
 }
 
 func TestRegisterAndUnregisterGVK(t *testing.T) {
 	logger := noopLogger()
-	client := setupFakeClient()
+	client, mapper := setupFakeClient(t)
 
 	config := Config{
 		Workers:         1,
@@ -95,7 +250,7 @@ func TestRegisterAndUnregisterGVK(t *testing.T) {
 		BurstLimit:      100,
 	}
 
-	dc := NewDynamicController(logger, config, client)
+	dc := NewDynamicController(logger, config, client, mapper)
 
 	gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"}
 
@@ -117,41 +272,41 @@ func TestRegisterAndUnregisterGVK(t *testing.T) {
 	})
 
 	// Register GVK
-	err := dc.Register(context.Background(), gvr, handlerFunc)
+	err := dc.Register(t.Context(), gvr, handlerFunc)
 	require.NoError(t, err)
 
-	_, exists := dc.informers.Load(gvr)
+	_, exists := dc.registrations[gvr]
 	assert.True(t, exists)
 
 	// Try to register again (should not fail)
-	err = dc.Register(context.Background(), gvr, handlerFunc)
+	err = dc.Register(t.Context(), gvr, handlerFunc)
 	assert.NoError(t, err)
 
 	// Unregister GVK
-	shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	shutdownContext, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	defer cancel()
 	err = dc.Deregister(shutdownContext, gvr)
 	require.NoError(t, err)
 
-	_, exists = dc.informers.Load(gvr)
+	_, exists = dc.registrations[gvr]
 	assert.False(t, exists)
 }
 
 func TestEnqueueObject(t *testing.T) {
 	logger := noopLogger()
-	client := setupFakeClient()
+	client, mapper := setupFakeClient(t)
 
 	dc := NewDynamicController(logger, Config{MinRetryDelay: 200 * time.Millisecond,
 		MaxRetryDelay: 1000 * time.Second,
 		RateLimit:     10,
-		BurstLimit:    100}, client)
+		BurstLimit:    100}, client, mapper)
 
-	obj := &unstructured.Unstructured{}
+	obj := &v1.PartialObjectMetadata{}
 	obj.SetName("test-object")
 	obj.SetNamespace("default")
 	obj.SetGroupVersionKind(schema.GroupVersionKind{Group: "test", Version: "v1", Kind: "Test"})
 
-	dc.enqueueObject(obj, "add")
+	dc.enqueueParent(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "resource"}, obj, "add")
 
 	assert.Equal(t, 1, dc.queue.Len())
 }
@@ -160,28 +315,30 @@ func TestInstanceUpdatePolicy(t *testing.T) {
 	logger := noopLogger()
 
 	scheme := runtime.NewScheme()
+	assert.NoError(t, v1.AddMetaToScheme(scheme))
 	gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"}
 	gvk := schema.GroupVersionKind{Group: "test", Version: "v1", Kind: "Test"}
+	scheme.AddKnownTypeWithName(gvk, &v1.PartialObjectMetadata{})
 
 	objs := make(map[string]runtime.Object)
 
-	obj1 := &unstructured.Unstructured{}
+	obj1 := &v1.PartialObjectMetadata{}
 	obj1.SetGroupVersionKind(gvk)
 	obj1.SetNamespace("default")
 	obj1.SetName("test-object-1")
 	objs[obj1.GetNamespace()+"/"+obj1.GetName()] = obj1
 
-	obj2 := &unstructured.Unstructured{}
+	obj2 := &v1.PartialObjectMetadata{}
 	obj2.SetGroupVersionKind(gvk)
 	obj2.SetNamespace("test-namespace")
 	obj2.SetName("test-object-2")
 	objs[obj2.GetNamespace()+"/"+obj2.GetName()] = obj2
 
-	client := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, map[schema.GroupVersionResource]string{
-		gvr: "TestList",
-	}, slices.Collect(maps.Values(objs))...)
+	client := fake.NewSimpleMetadataClient(scheme, obj1, obj2)
+	mapper := meta.NewDefaultRESTMapper(scheme.PreferredVersionAllGroups())
 
-	dc := NewDynamicController(logger, Config{}, client)
+	dc := NewDynamicController(logger, Config{}, client, mapper)
+	dc.ctx = t.Context() // simulate a start through dc.Run
 
 	handlerFunc := Handler(func(ctx context.Context, req controllerruntime.Request) error {
 		fmt.Println("reconciling instance", req)
@@ -189,7 +346,7 @@ func TestInstanceUpdatePolicy(t *testing.T) {
 	})
 
 	// simulate initial creation of the resource graph
-	err := dc.Register(context.Background(), gvr, handlerFunc)
+	err := dc.Register(t.Context(), gvr, handlerFunc)
 	assert.NoError(t, err)
 
 	// simulate reconciling the instances
@@ -200,14 +357,14 @@ func TestInstanceUpdatePolicy(t *testing.T) {
 	}
 
 	// simulate updating the resource graph
-	err = dc.Register(context.Background(), gvr, handlerFunc)
+	err = dc.Register(t.Context(), gvr, handlerFunc)
 	assert.NoError(t, err)
 
 	// check if the expected objects are queued
 	assert.Equal(t, dc.queue.Len(), 2)
 	for dc.queue.Len() > 0 {
 		name, _ := dc.queue.Get()
-		_, ok := objs[name.NamespacedKey]
+		_, ok := objs[name.NamespacedName.String()]
 		assert.True(t, ok)
 	}
 }
diff --git a/pkg/dynamiccontroller/internal/gvr_watch_test.go b/pkg/dynamiccontroller/internal/gvr_watch_test.go
new file mode 100644
index 0000000..38cabac
--- /dev/null
+++ b/pkg/dynamiccontroller/internal/gvr_watch_test.go
@@ -0,0 +1,147 @@
+// Copyright 2025 The Kube Resource Orchestrator Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package internal
+
+import (
+	"context"
+	"io"
+	"testing"
+	"time"
+
+	"github.com/go-logr/logr"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/metadata/fake"
+	"k8s.io/client-go/tools/cache"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+)
+
+func noopLogger() logr.Logger {
+	opts := zap.Options{DestWriter: io.Discard}
+	return zap.New(zap.UseFlagOptions(&opts))
+}
+
+func TestLazyInformer_AddAndRemoveHandler(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, v1.AddMetaToScheme(scheme))
+	client := fake.NewSimpleMetadataClient(scheme)
+	gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"}
+
+	ctx, cancel := context.WithCancel(t.Context())
+	defer cancel()
+
+	logger := noopLogger()
+	li := NewLazyInformer(client, gvr, time.Second, nil, logger)
+
+	// Adding first handler should start informer
+	err := li.AddHandler(ctx, "h1", cache.ResourceEventHandlerFuncs{})
+	require.NoError(t, err)
+
+	assert.NotNil(t, li.Informer())
+	assert.Len(t, li.handlers, 1)
+
+	// Removing it should stop informer
+	stopped, err := li.RemoveHandler("h1")
+	require.NoError(t, err)
+	assert.True(t, stopped)
+	assert.Nil(t, li.Informer())
+	assert.Empty(t, li.handlers)
+}
+
+func TestLazyInformer_MultipleHandlers(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, v1.AddMetaToScheme(scheme))
+	client := fake.NewSimpleMetadataClient(scheme)
+	gvr := schema.GroupVersionResource{Group: "group", Version: "v1", Resource: "resources"}
+
+	ctx, cancel := context.WithCancel(t.Context())
+	defer cancel()
+
+	logger := noopLogger()
+	li := NewLazyInformer(client, gvr, time.Second, nil, logger)
+
+	require.NoError(t, li.AddHandler(ctx, "a", cache.ResourceEventHandlerFuncs{}))
+	require.NoError(t, li.AddHandler(ctx, "b", cache.ResourceEventHandlerFuncs{}))
+	assert.Len(t, li.handlers, 2)
+
+	// Remove one, informer should remain
+	stopped, err := li.RemoveHandler("a")
+	require.NoError(t, err)
+	assert.False(t, stopped)
+	assert.Len(t, li.handlers, 1)
+	assert.NotNil(t, li.Informer())
+
+	// Remove second, informer should stop
+	stopped, err = li.RemoveHandler("b")
+	require.NoError(t, err)
+	assert.True(t, stopped)
+	assert.Nil(t, li.Informer())
+	assert.Empty(t, li.handlers)
+}
+
+func TestLazyInformer_Shutdown(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, v1.AddMetaToScheme(scheme))
+	client := fake.NewSimpleMetadataClient(scheme)
+	gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"}
+
+	ctx, cancel := context.WithCancel(t.Context())
+	defer cancel()
+
+	logger := noopLogger()
+	li := NewLazyInformer(client, gvr, time.Second, nil, logger)
+
+	require.NoError(t, li.AddHandler(ctx, "h", cache.ResourceEventHandlerFuncs{}))
+	assert.NotNil(t, li.Informer())
+	assert.Len(t, li.handlers, 1)
+
+	li.Shutdown()
+	assert.Nil(t, li.Informer())
+	assert.Empty(t, li.handlers)
+}
+
+func TestLazyInformer_RecreateAfterStop(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, v1.AddMetaToScheme(scheme))
+	client := fake.NewSimpleMetadataClient(scheme)
+	gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"}
+
+	ctx := t.Context()
+	logger := noopLogger()
+	li := NewLazyInformer(client, gvr, time.Second, nil, logger) // avoid tiny resync
+
+	// Add first handler and remove it -> stops informer
+	require.NoError(t, li.AddHandler(ctx, "h1", cache.ResourceEventHandlerFuncs{}))
+	stopped, err := li.RemoveHandler("h1")
+	require.NoError(t, err)
+	require.True(t, stopped)
+	assert.Nil(t, li.Informer())
+
+	// second removal should be a noop
+	stopped, err = li.RemoveHandler("h1")
+	assert.NoError(t, err)
+	assert.False(t, stopped)
+
+	// Add again — recreate context and informer
+	// ignore internal sync failure, just check that informer and handler exist
+	_ = li.AddHandler(ctx, "h2", cache.ResourceEventHandlerFuncs{})
+	if li.Informer() == nil {
+		t.Fatalf("informer should have been recreated")
+	}
+	assert.Len(t, li.handlers, 1)
+}
diff --git a/pkg/graph/builder_test.go b/pkg/graph/builder_test.go
index 8d18287..e7bc38b 100644
--- a/pkg/graph/builder_test.go
+++ b/pkg/graph/builder_test.go
@@ -24,7 +24,6 @@ import (
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/restmapper"
 
-	"github.com/kubernetes-sigs/kro/pkg/graph/emulator"
 	"github.com/kubernetes-sigs/kro/pkg/graph/variable"
 	"github.com/kubernetes-sigs/kro/pkg/testutil/generator"
 	"github.com/kubernetes-sigs/kro/pkg/testutil/k8s"
@@ -35,9 +34,8 @@ func TestGraphBuilder_Validation(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
 	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
-		schemaResolver:   fakeResolver,
-		restMapper:       restMapper,
-		resourceEmulator: emulator.NewEmulator(),
+		schemaResolver: fakeResolver,
+		restMapper:     restMapper,
 	}
 
 	tests := []struct {
@@ -233,7 +231,7 @@ func TestGraphBuilder_Validation(t *testing.T) {
 				}, nil, nil),
 			},
 			wantErr: true,
-			errMsg:  "failed to validate resource CEL expression",
+			errMsg:  "found unknown resources",
 		},
 		{
 			name: "valid VPC with valid conditional subnets",
@@ -434,8 +432,8 @@ func TestGraphBuilder_Validation(t *testing.T) {
 						"name": "string",
 					},
 					map[string]interface{}{
-						"abc": "${vpc.status.?unstructured.state}",
-						"cde": "${vpc.status.unstructured.?state}",
+						"state": "${vpc.status.?state}",
+						"vpcID": "${vpc.status.?vpcID}",
 					},
 				),
 				generator.WithResource("vpc", map[string]interface{}{
@@ -472,9 +470,8 @@ func TestGraphBuilder_DependencyValidation(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
 	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
-		schemaResolver:   fakeResolver,
-		restMapper:       restMapper,
-		resourceEmulator: emulator.NewEmulator(),
+		schemaResolver: fakeResolver,
+		restMapper:     restMapper,
 	}
 
 	tests := []struct {
@@ -610,7 +607,7 @@ func TestGraphBuilder_DependencyValidation(t *testing.T) {
 				}, nil, nil),
 			},
 			wantErr: true,
-			errMsg:  "undeclared reference to 'missingvpc'",
+			errMsg:  "found unknown resources",
 		},
 		{
 			name: "cyclic dependency",
@@ -1056,9 +1053,8 @@ func TestGraphBuilder_ExpressionParsing(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
 	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
-		schemaResolver:   fakeResolver,
-		restMapper:       restMapper,
-		resourceEmulator: emulator.NewEmulator(),
+		schemaResolver: fakeResolver,
+		restMapper:     restMapper,
 	}
 
 	tests := []struct {
@@ -1074,6 +1070,7 @@ func TestGraphBuilder_ExpressionParsing(t *testing.T) {
 					map[string]interface{}{
 						"replicas":         "integer | default=3",
 						"environment":      "string | default=dev",
+						"region":           "string | default=us-west-2",
 						"createMonitoring": "boolean | default=false",
 					},
 					nil,
@@ -1151,7 +1148,7 @@ func TestGraphBuilder_ExpressionParsing(t *testing.T) {
 							"environment":  "${schema.spec.environment}",
 							"cluster":      "${cluster.metadata.name}",
 							"combined":     "${cluster.metadata.name}-${schema.spec.environment}",
-							"two.statics":  "${schema.spec.environment}-static-${schema.spec.replicas}",
+							"two.statics":  "${schema.spec.environment}-${schema.spec.region}",
 							"two.dynamics": "${vpc.metadata.name}-${cluster.status.ackResourceMetadata.arn}",
 						},
 					},
@@ -1166,8 +1163,8 @@ func TestGraphBuilder_ExpressionParsing(t *testing.T) {
 										"value": "${cluster.status.ackResourceMetadata.arn}",
 									},
 									map[string]interface{}{
-										"name":  "REPLICAS",
-										"value": "${schema.spec.replicas}",
+										"name":  "REGION",
+										"value": "${schema.spec.region}",
 									},
 								},
 							},
@@ -1257,7 +1254,7 @@ func TestGraphBuilder_ExpressionParsing(t *testing.T) {
 					},
 					{
 						path:                 "metadata.labels[\"two.statics\"]",
-						expressions:          []string{"schema.spec.environment", "schema.spec.replicas"},
+						expressions:          []string{"schema.spec.environment", "schema.spec.region"},
 						kind:                 variable.ResourceVariableKindStatic,
 						standaloneExpression: false,
 					},
@@ -1275,7 +1272,7 @@ func TestGraphBuilder_ExpressionParsing(t *testing.T) {
 					},
 					{
 						path:                 "spec.containers[0].env[1].value",
-						expressions:          []string{"schema.spec.replicas"},
+						expressions:          []string{"schema.spec.region"},
 						kind:                 variable.ResourceVariableKindStatic,
 						standaloneExpression: true,
 					},
@@ -1391,7 +1388,6 @@ func validateVariables(t *testing.T, actual []*variable.ResourceField, expected
 
 	actualVars := make([]expectedVar, len(actual))
 	for i, v := range actual {
-		v.ExpectedSchema = nil
 		actualVars[i] = expectedVar{
 			path:                 v.Path,
 			expressions:          v.Expressions,
@@ -1403,6 +1399,390 @@ func validateVariables(t *testing.T, actual []*variable.ResourceField, expected
 	assert.ElementsMatch(t, expected, actualVars)
 }
 
+func TestGraphBuilder_CELTypeChecking(t *testing.T) {
+	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
+	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
+	builder := &Builder{
+		schemaResolver: fakeResolver,
+		restMapper:     restMapper,
+	}
+
+	tests := []struct {
+		name                        string
+		resourceGraphDefinitionOpts []generator.ResourceGraphDefinitionOption
+		wantErr                     bool
+		errMsg                      string
+	}{
+		// Test 1: ObjectMeta field access validation
+		{
+			name: "valid access to metadata.name",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					map[string]interface{}{
+						"podName": "${pod.metadata.name}",
+					},
+				),
+				generator.WithResource("pod", map[string]interface{}{
+					"apiVersion": "v1",
+					"kind":       "Pod",
+					"metadata": map[string]interface{}{
+						"name": "test-pod",
+					},
+					"spec": map[string]interface{}{
+						"containers": []interface{}{
+							map[string]interface{}{
+								"name":  "nginx",
+								"image": "nginx:latest",
+							},
+						},
+					},
+				}, nil, nil),
+			},
+			wantErr: false,
+		},
+		{
+			name: "valid access to metadata.labels",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, nil),
+				generator.WithResource("subnet", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "Subnet",
+					"metadata": map[string]interface{}{
+						"name":   "test-subnet",
+						"labels": "${vpc.metadata.labels}",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlock": "10.0.1.0/24",
+						"vpcID":     "${vpc.status.vpcID}",
+					},
+				}, nil, nil),
+			},
+			wantErr: false,
+		},
+		{
+			name: "valid access to metadata.annotations",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, nil),
+				generator.WithResource("subnet", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "Subnet",
+					"metadata": map[string]interface{}{
+						"name":        "test-subnet",
+						"annotations": "${vpc.metadata.annotations}",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlock": "10.0.1.0/24",
+						"vpcID":     "${vpc.status.vpcID}",
+					},
+				}, nil, nil),
+			},
+			wantErr: false,
+		},
+		{
+			name: "invalid access to non-existent metadata field",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					map[string]interface{}{
+						"invalid": "${pod.metadata.nonExistentField}",
+					},
+				),
+				generator.WithResource("pod", map[string]interface{}{
+					"apiVersion": "v1",
+					"kind":       "Pod",
+					"metadata": map[string]interface{}{
+						"name": "test-pod",
+					},
+					"spec": map[string]interface{}{
+						"containers": []interface{}{
+							map[string]interface{}{
+								"name":  "nginx",
+								"image": "nginx:latest",
+							},
+						},
+					},
+				}, nil, nil),
+			},
+			wantErr: true,
+			errMsg:  "undefined field 'nonExistentField'",
+		},
+
+		// Test 2: readyWhen and includeWhen type checking
+		{
+			name: "readyWhen returning non-boolean type",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, []string{"${vpc.status.vpcID}"}, nil), // Returns string, not bool
+			},
+			wantErr: true,
+			errMsg:  "must return bool",
+		},
+		{
+			name: "includeWhen returning non-boolean type",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"count": "integer",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, []string{"${schema.spec.count}"}), // Returns integer, not bool
+			},
+			wantErr: true,
+			errMsg:  "must return bool",
+		},
+		{
+			name: "valid readyWhen with boolean expression",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, []string{"${vpc.status.state == 'available'}"}, nil),
+			},
+			wantErr: false,
+		},
+		{
+			name: "valid includeWhen with boolean expression",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"enabled": "boolean",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, []string{"${schema.spec.enabled}"}),
+			},
+			wantErr: false,
+		},
+
+		// Test 3: Optional type handling
+		{
+			name: "valid optional type in status field",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					map[string]interface{}{
+						"state": "${vpc.status.?state}",
+					},
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, nil),
+			},
+			wantErr: false,
+		},
+		{
+			name: "optional type mismatch with helpful error",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"count": "integer",
+					},
+					map[string]interface{}{
+						"name": "${vpc.status.?state}", // state is string
+					},
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, nil),
+			},
+			wantErr: false, // optional string is assignable to string
+		},
+
+		// Test 5: Metadata field type mismatches
+		{
+			name: "using metadata.name as object instead of string",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, nil),
+				generator.WithResource("subnet", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "Subnet",
+					"metadata": map[string]interface{}{
+						"name": "test-subnet",
+					},
+					"spec": map[string]interface{}{
+						"vpcID": "${vpc.metadata.name.field}", // name is string, not object
+					},
+				}, nil, nil),
+			},
+			wantErr: true,
+			errMsg:  "does not support field selection",
+		},
+		{
+			name: "using metadata.labels value with correct type",
+			resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
+				generator.WithSchema(
+					"Test", "v1alpha1",
+					map[string]interface{}{
+						"name": "string",
+					},
+					nil,
+				),
+				generator.WithResource("vpc", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "VPC",
+					"metadata": map[string]interface{}{
+						"name": "test-vpc",
+						"labels": map[string]interface{}{
+							"env": "prod",
+						},
+					},
+					"spec": map[string]interface{}{
+						"cidrBlocks": []interface{}{"10.0.0.0/16"},
+					},
+				}, nil, nil),
+				generator.WithResource("subnet", map[string]interface{}{
+					"apiVersion": "ec2.services.k8s.aws/v1alpha1",
+					"kind":       "Subnet",
+					"metadata": map[string]interface{}{
+						"name": "${vpc.metadata.labels['env']}-subnet", // Accessing map value
+					},
+					"spec": map[string]interface{}{
+						"cidrBlock": "10.0.1.0/24",
+						"vpcID":     "${vpc.status.vpcID}",
+					},
+				}, nil, nil),
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			rgd := generator.NewResourceGraphDefinition("test-cel-types", tt.resourceGraphDefinitionOpts...)
+			_, err := builder.NewResourceGraphDefinition(rgd)
+
+			if tt.wantErr {
+				assert.Error(t, err)
+				assert.Contains(t, err.Error(), tt.errMsg)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
 func TestNewBuilder(t *testing.T) {
 	builder, err := NewBuilder(&rest.Config{}, &http.Client{})
 	assert.Nil(t, err)
@@ -1413,9 +1793,8 @@ func Test_ValidateOpenAPISchema(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
 	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
-		schemaResolver:   fakeResolver,
-		restMapper:       restMapper,
-		resourceEmulator: emulator.NewEmulator(),
+		schemaResolver: fakeResolver,
+		restMapper:     restMapper,
 	}
 
 	tests := []struct {
diff --git a/pkg/graph/parser/cel_test.go b/pkg/graph/parser/cel_test.go
index c414f86..b194e32 100644
--- a/pkg/graph/parser/cel_test.go
+++ b/pkg/graph/parser/cel_test.go
@@ -167,6 +167,30 @@ func TestExtractExpressions(t *testing.T) {
 			want:    []string{},
 			wantErr: true,
 		},
+		{
+			name:    "Ternary with empty map literal",
+			input:   "${condition ? value : {}}",
+			want:    []string{"condition ? value : {}"},
+			wantErr: false,
+		},
+		{
+			name:    "Ternary with empty map on both sides",
+			input:   "${condition ? {} : {}}",
+			want:    []string{"condition ? {} : {}"},
+			wantErr: false,
+		},
+		{
+			name:    "Complex ternary with empty map (real world example)",
+			input:   "${schema.spec.deployment.includeAnnotations ? schema.spec.deployment.annotations : {}}",
+			want:    []string{"schema.spec.deployment.includeAnnotations ? schema.spec.deployment.annotations : {}"},
+			wantErr: false,
+		},
+		{
+			name:    "Ternary with has() and empty map",
+			input:   "${has(schema.annotations) && includeAnnotations ? schema.annotations : {}}",
+			want:    []string{"has(schema.annotations) && includeAnnotations ? schema.annotations : {}"},
+			wantErr: false,
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/pkg/graph/parser/parser_test.go b/pkg/graph/parser/parser_test.go
index 5700675..410fdb5 100644
--- a/pkg/graph/parser/parser_test.go
+++ b/pkg/graph/parser/parser_test.go
@@ -25,6 +25,17 @@ import (
 	"github.com/kubernetes-sigs/kro/pkg/graph/variable"
 )
 
+// newSchema creates a spec.Schema with properly initialized VendorExtensible
+// to avoid nil pointer panics in the OpenAPI library
+func newSchema(props spec.SchemaProps) spec.Schema {
+	return spec.Schema{
+		SchemaProps: props,
+		VendorExtensible: spec.VendorExtensible{
+			Extensions: spec.Extensions{},
+		},
+	}
+}
+
 func TestParseResource(t *testing.T) {
 	t.Run("Simple resource with various types", func(t *testing.T) {
 		resource := map[string]interface{}{
@@ -133,21 +144,21 @@ func TestParseResource(t *testing.T) {
 		}
 
 		expectedExpressions := []variable.FieldDescriptor{
-			{Path: "stringField", Expressions: []string{"string.value"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "intField", Expressions: []string{"int.value"}, ExpectedTypes: []string{"integer"}, StandaloneExpression: true},
-			{Path: "boolField", Expressions: []string{"bool.value"}, ExpectedTypes: []string{"boolean"}, StandaloneExpression: true},
-			{Path: "nestedObject.nestedString", Expressions: []string{"nested.string"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "nestedObject.nestedStringMultiple", Expressions: []string{"nested.string1", "nested.string2"}, ExpectedTypes: []string{"string"}, StandaloneExpression: false},
-			{Path: "simpleArray[0]", Expressions: []string{"array[0]"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "simpleArray[1]", Expressions: []string{"array[1]"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "mapField.key1", Expressions: []string{"map.key1"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "mapField.key2", Expressions: []string{"map.key2"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "specialCharacters.simpleAnnotation", Expressions: []string{"simpleannotation"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "specialCharacters[\"doted.annotation.key\"]", Expressions: []string{"dotedannotationvalue"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "specialCharacters[\"\"]", Expressions: []string{"emptyannotation"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "specialCharacters[\"array.name.with.dots\"][0]", Expressions: []string{"value"}, ExpectedTypes: []string{"string"}, StandaloneExpression: true},
-			{Path: "schemalessField.something", Expressions: []string{"schemaless.value"}, ExpectedTypes: []string{"any"}, StandaloneExpression: true},
-			{Path: "schemalessField.nestedSomething.nested", Expressions: []string{"schemaless.nested.value"}, ExpectedTypes: []string{"any"}, StandaloneExpression: true},
+			{Path: "stringField", Expressions: []string{"string.value"}, StandaloneExpression: true},
+			{Path: "intField", Expressions: []string{"int.value"}, StandaloneExpression: true},
+			{Path: "boolField", Expressions: []string{"bool.value"}, StandaloneExpression: true},
+			{Path: "nestedObject.nestedString", Expressions: []string{"nested.string"}, StandaloneExpression: true},
+			{Path: "nestedObject.nestedStringMultiple", Expressions: []string{"nested.string1", "nested.string2"}, StandaloneExpression: false},
+			{Path: "simpleArray[0]", Expressions: []string{"array[0]"}, StandaloneExpression: true},
+			{Path: "simpleArray[1]", Expressions: []string{"array[1]"}, StandaloneExpression: true},
+			{Path: "mapField.key1", Expressions: []string{"map.key1"}, StandaloneExpression: true},
+			{Path: "mapField.key2", Expressions: []string{"map.key2"}, StandaloneExpression: true},
+			{Path: "specialCharacters.simpleAnnotation", Expressions: []string{"simpleannotation"}, StandaloneExpression: true},
+			{Path: "specialCharacters[\"doted.annotation.key\"]", Expressions: []string{"dotedannotationvalue"}, StandaloneExpression: true},
+			{Path: "specialCharacters[\"\"]", Expressions: []string{"emptyannotation"}, StandaloneExpression: true},
+			{Path: "specialCharacters[\"array.name.with.dots\"][0]", Expressions: []string{"value"}, StandaloneExpression: true},
+			{Path: "schemalessField.something", Expressions: []string{"schemaless.value"}, StandaloneExpression: true},
+			{Path: "schemalessField.nestedSomething.nested", Expressions: []string{"schemaless.nested.value"}, StandaloneExpression: true},
 		}
 
 		expressions, err := ParseResource(resource, schema)
@@ -180,13 +191,6 @@ func TestParseResource(t *testing.T) {
 				)
 			}
 
-			if !areEqualSlices(actual.ExpectedTypes, expected.ExpectedTypes) {
-				t.Errorf(
-					"Expression[%d] types mismatch for path %s:\n  got:  %v\n  want: %v",
-					i, expected.Path, actual.ExpectedTypes, expected.ExpectedTypes,
-				)
-			}
-
 			if actual.StandaloneExpression != expected.StandaloneExpression {
 				t.Errorf(
 					"Expression[%d] StandaloneExpression mismatch for path %s:\n  got:  %v\n  want: %v",
@@ -255,7 +259,7 @@ func TestTypeMismatches(t *testing.T) {
 				},
 			},
 			wantErr:       true,
-			expectedError: "unexpected type for path stringField",
+			expectedError: "expected string type for path stringField, got integer",
 		},
 		{
 			name: "Boolean instead of number",
@@ -271,7 +275,7 @@ func TestTypeMismatches(t *testing.T) {
 				},
 			},
 			wantErr:       true,
-			expectedError: "expected number type for path numberField, got bool",
+			expectedError: "expected number type for path numberField, got boolean",
 		},
 		{
 			name: "Array instead of object",
@@ -468,57 +472,46 @@ func TestParseWithExpectedSchema(t *testing.T) {
 		},
 	}
 
-	stringFieldSchema := spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}}
-	objectFieldSchema := spec.Schema{
-		SchemaProps: spec.SchemaProps{
-			Type: []string{"object"},
-			Properties: map[string]spec.Schema{
-				"key1": {SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
-				"key2": {SchemaProps: spec.SchemaProps{Type: []string{"integer"}}},
-			},
+	stringFieldSchema := newSchema(spec.SchemaProps{Type: []string{"string"}})
+	objectFieldSchema := newSchema(spec.SchemaProps{
+		Type: []string{"object"},
+		Properties: map[string]spec.Schema{
+			"key1": newSchema(spec.SchemaProps{Type: []string{"string"}}),
+			"key2": newSchema(spec.SchemaProps{Type: []string{"integer"}}),
 		},
-	}
-	nestedObjectFieldSchema := spec.Schema{
-		SchemaProps: spec.SchemaProps{
-			Type: []string{"object"},
-			Properties: map[string]spec.Schema{
-				"nestedString": {
-					SchemaProps: spec.SchemaProps{Type: []string{"string"}},
-				},
-				"nestedObject": {
-					SchemaProps: spec.SchemaProps{
-						Type: []string{"object"},
-						Properties: map[string]spec.Schema{
-							"deepNested": {
-								SchemaProps: spec.SchemaProps{Type: []string{"string"}},
-							},
-						},
-					},
+	})
+	nestedObjectFieldSchema := newSchema(spec.SchemaProps{
+		Type: []string{"object"},
+		Properties: map[string]spec.Schema{
+			"nestedString": newSchema(spec.SchemaProps{Type: []string{"string"}}),
+			"nestedObject": newSchema(spec.SchemaProps{
+				Type: []string{"object"},
+				Properties: map[string]spec.Schema{
+					"deepNested": newSchema(spec.SchemaProps{Type: []string{"string"}}),
 				},
-			},
+			}),
 		},
-	}
-	arrayFieldSchema := spec.Schema{
-		SchemaProps: spec.SchemaProps{
-			Type: []string{"array"},
-			Items: &spec.SchemaOrArray{
-				Schema: &spec.Schema{
-					SchemaProps: spec.SchemaProps{
-						Type: []string{"object"},
-						Properties: map[string]spec.Schema{
-							"objectInArray": {
-								SchemaProps: spec.SchemaProps{Type: []string{"string"}},
-							},
-						},
-						AdditionalProperties: &spec.SchemaOrBool{Allows: true},
-					},
+	})
+	arrayFieldItemSchema := newSchema(spec.SchemaProps{
+		Type: []string{"object"},
+		Properties: map[string]spec.Schema{
+			"objectInArray": newSchema(spec.SchemaProps{Type: []string{"string"}}),
+		},
+		AdditionalProperties: &spec.SchemaOrBool{
+			Allows: true,
+			Schema: &spec.Schema{
+				VendorExtensible: spec.VendorExtensible{
+					Extensions: spec.Extensions{},
 				},
 			},
 		},
-	}
-	nestedObjectNestedStringSchema := nestedObjectFieldSchema.Properties["nestedString"]
-	deepNestedSchema := nestedObjectFieldSchema.Properties["nestedObject"].Properties["deepNested"]
-	objectInArraySchema := arrayFieldSchema.Items.Schema.Properties["objectInArray"]
+	})
+	arrayFieldSchema := newSchema(spec.SchemaProps{
+		Type: []string{"array"},
+		Items: &spec.SchemaOrArray{
+			Schema: &arrayFieldItemSchema,
+		},
+	})
 
 	schema := &spec.Schema{
 		SchemaProps: spec.SchemaProps{
@@ -530,6 +523,9 @@ func TestParseWithExpectedSchema(t *testing.T) {
 				"arrayField":        arrayFieldSchema,
 			},
 		},
+		VendorExtensible: spec.VendorExtensible{
+			Extensions: spec.Extensions{},
+		},
 	}
 
 	expressions, err := ParseResource(resource, schema)
@@ -538,12 +534,12 @@ func TestParseWithExpectedSchema(t *testing.T) {
 	}
 
 	expectedExpressions := map[string]variable.FieldDescriptor{
-		"stringField":                               {Path: "stringField", Expressions: []string{"string.value"}, ExpectedTypes: []string{"string"}, ExpectedSchema: &stringFieldSchema, StandaloneExpression: true},
-		"objectField":                               {Path: "objectField", Expressions: []string{"object.value"}, ExpectedTypes: []string{"object"}, ExpectedSchema: &objectFieldSchema, StandaloneExpression: true},
-		"nestedObjectField.nestedString":            {Path: "nestedObjectField.nestedString", Expressions: []string{"nested.string"}, ExpectedTypes: []string{"string"}, ExpectedSchema: &nestedObjectNestedStringSchema, StandaloneExpression: true},
-		"nestedObjectField.nestedObject.deepNested": {Path: "nestedObjectField.nestedObject.deepNested", Expressions: []string{"deep.nested"}, ExpectedTypes: []string{"string"}, ExpectedSchema: &deepNestedSchema, StandaloneExpression: true},
-		"arrayField[0]":                             {Path: "arrayField[0]", Expressions: []string{"array[0]"}, ExpectedTypes: []string{"object"}, ExpectedSchema: arrayFieldSchema.Items.Schema, StandaloneExpression: true},
-		"arrayField[1].objectInArray":               {Path: "arrayField[1].objectInArray", Expressions: []string{"object.in.array"}, ExpectedTypes: []string{"string"}, ExpectedSchema: &objectInArraySchema, StandaloneExpression: true},
+		"stringField":                               {Path: "stringField", Expressions: []string{"string.value"}, StandaloneExpression: true},
+		"objectField":                               {Path: "objectField", Expressions: []string{"object.value"}, StandaloneExpression: true},
+		"nestedObjectField.nestedString":            {Path: "nestedObjectField.nestedString", Expressions: []string{"nested.string"}, StandaloneExpression: true},
+		"nestedObjectField.nestedObject.deepNested": {Path: "nestedObjectField.nestedObject.deepNested", Expressions: []string{"deep.nested"}, StandaloneExpression: true},
+		"arrayField[0]":                             {Path: "arrayField[0]", Expressions: []string{"array[0]"}, StandaloneExpression: true},
+		"arrayField[1].objectInArray":               {Path: "arrayField[1].objectInArray", Expressions: []string{"object.in.array"}, StandaloneExpression: true},
 	}
 
 	if len(expressions) != len(expectedExpressions) {
@@ -560,17 +556,9 @@ func TestParseWithExpectedSchema(t *testing.T) {
 		if !reflect.DeepEqual(expr.Expressions, expected.Expressions) {
 			t.Errorf("Path %s: expected expressions %v, got %v", expr.Path, expected.Expressions, expr.Expressions)
 		}
-		if !areEqualSlices(expr.ExpectedTypes, expected.ExpectedTypes) {
-			t.Errorf("Path %s: expected type %s, got %s", expr.Path, expected.ExpectedTypes, expr.ExpectedTypes)
-		}
 		if expr.StandaloneExpression != expected.StandaloneExpression {
 			t.Errorf("Path %s: expected OneShotCEL %v, got %v", expr.Path, expected.StandaloneExpression, expr.StandaloneExpression)
 		}
-		if !reflect.DeepEqual(expr.ExpectedSchema, expected.ExpectedSchema) {
-			t.Errorf("Path %s: schema mismatch", expr.Path)
-			t.Errorf("Expected schema: %+v", expected.ExpectedSchema)
-			t.Errorf("Got schema: %+v", expr.ExpectedSchema)
-		}
 
 		// remove the matched expression from the map
 		// NOTE(a-hilaly): since the object is a map, the order of the expressions is not guaranteed
@@ -612,7 +600,7 @@ func TestParserEdgeCases(t *testing.T) {
 				},
 			},
 			resource:      42,
-			expectedError: "unexpected type for path : int, expectedTypes: [string]",
+			expectedError: "expected string type for path , got integer",
 		},
 		{
 			name: "Type mismatch: object/array",
@@ -652,7 +640,7 @@ func TestParserEdgeCases(t *testing.T) {
 				},
 			},
 			resource:      true,
-			expectedError: "expected number type for path , got bool",
+			expectedError: "expected number type for path , got boolean",
 		},
 		{
 			name: "Type mismatch: array/object",
@@ -810,22 +798,24 @@ func TestJoinPathAndFieldName(t *testing.T) {
 
 func TestPartScalerTypesShortSpecTypes(t *testing.T) {
 	tests := []struct {
-		name          string
-		shortSpecType []string
-		field         interface{}
+		name   string
+		schema *spec.Schema
+		field  interface{}
 	}{
-		{"int short type for integer", []string{"int"}, 42},
-		{"bool short type for boolean", []string{"bool"}, true},
+		{"int short type for integer", &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"int"}}}, 42},
+		{"bool short type for boolean", &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"bool"}}}, true},
 	}
 
-	dummySchema := &spec.Schema{}
-
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := parseScalarTypes(tt.field, dummySchema, "spec.someitem", tt.shortSpecType)
+			expectedTypes, err := getExpectedTypes(tt.schema)
+			if err != nil {
+				t.Fatalf("getExpectedTypes() error = %v", err)
+			}
+			_, err = parseScalarTypes(tt.field, tt.schema, "spec.someitem", expectedTypes)
 			if err != nil {
-				t.Errorf("Expected %T resolved for %s, but got error: %s",
-					tt.field, tt.shortSpecType, err.Error())
+				t.Errorf("Expected %T resolved for %v, but got error: %s",
+					tt.field, tt.schema.Type, err.Error())
 			}
 		})
 	}
@@ -877,7 +867,7 @@ func TestXKubernetesIntOrString(t *testing.T) {
 				"myField": true,
 			},
 			wantErr:    true,
-			wantErrMsg: "expected integer type for path myField, got bool",
+			wantErrMsg: "expected string or integer type for path myField, got boolean",
 		},
 	}
 
@@ -958,7 +948,7 @@ func TestNestedXKubernetesIntOrString(t *testing.T) {
 					},
 				},
 				wantErr:       true,
-				expectedError: "expected integer type for path outerObject.nestedField, got bool",
+				expectedError: "expected string or integer type for path outerObject.nestedField, got boolean",
 			},
 		}
 
@@ -1050,7 +1040,7 @@ func TestOneOfAndAnyOf(t *testing.T) {
 				"field": true,
 			},
 			wantErr:       true,
-			expectedError: "expected integer type for path field, got bool",
+			expectedError: "expected string or integer type for path field, got boolean",
 		},
 		{
 			name: "Valid AnyOf - matches one schema (string)",
@@ -1117,7 +1107,7 @@ func TestOneOfAndAnyOf(t *testing.T) {
 				"field": true,
 			},
 			wantErr:       true,
-			expectedError: "expected integer type for path field, got bool",
+			expectedError: "expected string or integer type for path field, got boolean",
 		},
 		{
 			name: "Nested OneOf - valid nested schema (string)",
@@ -1180,7 +1170,7 @@ func TestOneOfAndAnyOf(t *testing.T) {
 				},
 			},
 			wantErr:       true,
-			expectedError: "expected integer type for path nestedField.innerField, got bool",
+			expectedError: "expected string or integer type for path nestedField.innerField, got boolean",
 		},
 	}
 
@@ -1271,7 +1261,6 @@ func TestOneOfWithStructuralConstraints(t *testing.T) {
 		expected := variable.FieldDescriptor{
 			Path:                 "networkRef.name",
 			Expressions:          []string{"network.metadata.name"},
-			ExpectedTypes:        []string{"string"},
 			StandaloneExpression: true,
 		}
 
@@ -1283,10 +1272,6 @@ func TestOneOfWithStructuralConstraints(t *testing.T) {
 			t.Errorf("Expressions mismatch: got %v, want %v", expressions[0].Expressions, expected.Expressions)
 		}
 
-		if !reflect.DeepEqual(expressions[0].ExpectedTypes, expected.ExpectedTypes) {
-			t.Errorf("ExpectedTypes mismatch: got %v, want %v", expressions[0].ExpectedTypes, expected.ExpectedTypes)
-		}
-
 		if expressions[0].StandaloneExpression != expected.StandaloneExpression {
 			t.Errorf("StandaloneExpression mismatch: got %v, want %v", expressions[0].StandaloneExpression, expected.StandaloneExpression)
 		}
@@ -1365,7 +1350,6 @@ func TestOneOfWithStructuralConstraints(t *testing.T) {
 		expected := variable.FieldDescriptor{
 			Path:                 "networkRef.external",
 			Expressions:          []string{"network.selfLink"},
-			ExpectedTypes:        []string{"string"},
 			StandaloneExpression: true,
 		}
 
@@ -1375,9 +1359,6 @@ func TestOneOfWithStructuralConstraints(t *testing.T) {
 		if !reflect.DeepEqual(expressions[0].Expressions, expected.Expressions) {
 			t.Errorf("Expected expressions %v, got %v", expected.Expressions, expressions[0].Expressions)
 		}
-		if !reflect.DeepEqual(expressions[0].ExpectedTypes, expected.ExpectedTypes) {
-			t.Errorf("Expected types %v, got %v", expected.ExpectedTypes, expressions[0].ExpectedTypes)
-		}
 
 		if expressions[0].StandaloneExpression != expected.StandaloneExpression {
 			t.Errorf("StandaloneExpression mismatch: got %v, want %v", expressions[0].StandaloneExpression, expected.StandaloneExpression)
@@ -1413,7 +1394,6 @@ func TestPreserveUnknownFields(t *testing.T) {
 				{
 					Path:                 "spec.template",
 					Expressions:          []string{"template.value"},
-					ExpectedTypes:        []string{"any"},
 					StandaloneExpression: true,
 				},
 			},
@@ -1444,7 +1424,6 @@ func TestPreserveUnknownFields(t *testing.T) {
 				{
 					Path:                 "spec.template.nested[0].key",
 					Expressions:          []string{"template.value"},
-					ExpectedTypes:        []string{"any"},
 					StandaloneExpression: true,
 				},
 			},
@@ -1512,25 +1491,21 @@ func TestPreserveUnknownFields(t *testing.T) {
 				{
 					Path:                 "program.resources.app.properties.spec.name",
 					Expressions:          []string{"schema.spec.name"},
-					ExpectedTypes:        []string{"any"},
 					StandaloneExpression: true,
 				},
 				{
 					Path:                 "program.resources.app.properties.spec.region",
 					Expressions:          []string{"schema.spec.region"},
-					ExpectedTypes:        []string{"any"},
 					StandaloneExpression: true,
 				},
 				{
 					Path:                 "program.resources.app.properties.spec.services[0].name",
 					Expressions:          []string{"schema.spec.name"},
-					ExpectedTypes:        []string{"any"},
 					StandaloneExpression: false,
 				},
 				{
 					Path:                 "program.resources.app.properties.spec.services[0].instanceCount",
 					Expressions:          []string{"schema.spec.instanceCount"},
-					ExpectedTypes:        []string{"any"},
 					StandaloneExpression: true,
 				},
 			},
@@ -1582,9 +1557,6 @@ func TestPreserveUnknownFields(t *testing.T) {
 					if !reflect.DeepEqual(actualExpr.Expressions, expectedExpr.Expressions) {
 						t.Errorf("Path %s: expected expressions %v, got %v", path, expectedExpr.Expressions, actualExpr.Expressions)
 					}
-					if !reflect.DeepEqual(actualExpr.ExpectedTypes, expectedExpr.ExpectedTypes) {
-						t.Errorf("Path %s: expected types %v, got %v", path, expectedExpr.ExpectedTypes, actualExpr.ExpectedTypes)
-					}
 					if actualExpr.StandaloneExpression != expectedExpr.StandaloneExpression {
 						t.Errorf("Path %s: expected StandaloneExpression %v, got %v", path, expectedExpr.StandaloneExpression, actualExpr.StandaloneExpression)
 					}
@@ -1673,3 +1645,128 @@ func TestCollectTypesFromSubSchemas(t *testing.T) {
 		})
 	}
 }
+
+// TestEmptyBracesInExpressions tests the regression where strings.Trim() was
+// incorrectly stripping {} from expressions. This bug affected ternary expressions
+// with empty map literals like: condition ? value : {}
+func TestEmptyBracesInExpressions(t *testing.T) {
+	testCases := []struct {
+		name             string
+		resource         map[string]interface{}
+		schema           *spec.Schema
+		expectedExprPath string // Path where we expect to find the expression
+		expectedExpr     string // The exact expression we expect (without ${})
+	}{
+		{
+			name: "Ternary with empty map literal",
+			resource: map[string]interface{}{
+				"annotations": "${includeAnnotations ? annotations : {}}",
+			},
+			schema: &spec.Schema{
+				SchemaProps: spec.SchemaProps{
+					Type: []string{"object"},
+					Properties: map[string]spec.Schema{
+						"annotations": {
+							SchemaProps: spec.SchemaProps{
+								Type: []string{"object"},
+								AdditionalProperties: &spec.SchemaOrBool{
+									Allows: true,
+									Schema: &spec.Schema{
+										SchemaProps: spec.SchemaProps{Type: []string{"string"}},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedExprPath: "annotations",
+			expectedExpr:     "includeAnnotations ? annotations : {}",
+		},
+		{
+			name: "Complex ternary with has() and empty map",
+			resource: map[string]interface{}{
+				"metadata": map[string]interface{}{
+					"annotations": "${has(schema.annotations) && includeAnnotations ? schema.annotations : {}}",
+				},
+			},
+			schema: &spec.Schema{
+				SchemaProps: spec.SchemaProps{
+					Type: []string{"object"},
+					Properties: map[string]spec.Schema{
+						"metadata": {
+							SchemaProps: spec.SchemaProps{
+								Type: []string{"object"},
+								Properties: map[string]spec.Schema{
+									"annotations": {
+										SchemaProps: spec.SchemaProps{
+											Type: []string{"object"},
+											AdditionalProperties: &spec.SchemaOrBool{
+												Allows: true,
+												Schema: &spec.Schema{
+													SchemaProps: spec.SchemaProps{Type: []string{"string"}},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedExprPath: "metadata.annotations",
+			expectedExpr:     "has(schema.annotations) && includeAnnotations ? schema.annotations : {}",
+		},
+		{
+			name: "Ternary with empty maps on both sides",
+			resource: map[string]interface{}{
+				"config": "${condition ? {} : {}}",
+			},
+			schema: &spec.Schema{
+				SchemaProps: spec.SchemaProps{
+					Type: []string{"object"},
+					Properties: map[string]spec.Schema{
+						"config": {
+							SchemaProps: spec.SchemaProps{
+								Type: []string{"object"},
+							},
+						},
+					},
+				},
+			},
+			expectedExprPath: "config",
+			expectedExpr:     "condition ? {} : {}",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			fields, err := ParseResource(tc.resource, tc.schema)
+			if err != nil {
+				t.Fatalf("ParseResource() error = %v", err)
+			}
+
+			// Find the field descriptor for the expected path
+			found := false
+			for _, field := range fields {
+				if field.Path == tc.expectedExprPath {
+					found = true
+					if len(field.Expressions) != 1 {
+						t.Errorf("Expected 1 expression, got %d", len(field.Expressions))
+						continue
+					}
+					if field.Expressions[0] != tc.expectedExpr {
+						t.Errorf("Expression mismatch:\ngot:  %q\nwant: %q", field.Expressions[0], tc.expectedExpr)
+					}
+					if !field.StandaloneExpression {
+						t.Error("Expected StandaloneExpression to be true")
+					}
+				}
+			}
+			if !found {
+				t.Errorf("Expected to find field descriptor for path %q", tc.expectedExprPath)
+			}
+		})
+	}
+}
diff --git a/pkg/graph/parser/schemaless_test.go b/pkg/graph/parser/schemaless_test.go
index f64e015..e7429da 100644
--- a/pkg/graph/parser/schemaless_test.go
+++ b/pkg/graph/parser/schemaless_test.go
@@ -31,7 +31,6 @@ func areEqualExpressionFields(a, b []variable.FieldDescriptor) bool {
 
 	for i := range a {
 		if !equalStrings(a[i].Expressions, b[i].Expressions) ||
-			!areEqualSlices(a[i].ExpectedTypes, b[i].ExpectedTypes) ||
 			a[i].Path != b[i].Path ||
 			a[i].StandaloneExpression != b[i].StandaloneExpression {
 			return false
@@ -86,7 +85,6 @@ func TestParseSchemalessResource(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{"resource.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "field",
 					StandaloneExpression: true,
 				},
@@ -103,7 +101,6 @@ func TestParseSchemalessResource(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{"nested.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "outer.inner",
 					StandaloneExpression: true,
 				},
@@ -121,13 +118,11 @@ func TestParseSchemalessResource(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{"array[0]"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "array[0]",
 					StandaloneExpression: true,
 				},
 				{
 					Expressions:          []string{"array[1]"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "array[1]",
 					StandaloneExpression: true,
 				},
@@ -141,9 +136,8 @@ func TestParseSchemalessResource(t *testing.T) {
 			},
 			want: []variable.FieldDescriptor{
 				{
-					Expressions:   []string{"expr1", "expr2"},
-					ExpectedTypes: []string{"any"},
-					Path:          "field",
+					Expressions: []string{"expr1", "expr2"},
+					Path:        "field",
 				},
 			},
 			wantErr: false,
@@ -164,13 +158,11 @@ func TestParseSchemalessResource(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{"string.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "string",
 					StandaloneExpression: true,
 				},
 				{
 					Expressions:          []string{"array.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "nested.array[0]",
 					StandaloneExpression: true,
 				},
@@ -228,7 +220,6 @@ func TestParseSchemalessResourceEdgeCases(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{"deeply.nested.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "level1.level2.level3.level4",
 					StandaloneExpression: true,
 				},
@@ -250,13 +241,11 @@ func TestParseSchemalessResourceEdgeCases(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{"expr1"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "array[0]",
 					StandaloneExpression: true,
 				},
 				{
 					Expressions:          []string{"expr2"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "array[3].nested",
 					StandaloneExpression: true,
 				},
@@ -272,13 +261,11 @@ func TestParseSchemalessResourceEdgeCases(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{""},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "empty1",
 					StandaloneExpression: true,
 				},
 				{
 					Expressions:          []string{"    "},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "empty2",
 					StandaloneExpression: true,
 				},
@@ -322,36 +309,30 @@ func TestParseSchemalessResourceEdgeCases(t *testing.T) {
 			want: []variable.FieldDescriptor{
 				{
 					Expressions:          []string{"string.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "string",
 					StandaloneExpression: true,
 				},
 				{
 					Expressions:          []string{"array.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "nested.array[0]",
 					StandaloneExpression: true,
 				},
 				{
-					Expressions:   []string{"expr1", "expr2"},
-					ExpectedTypes: []string{"any"},
-					Path:          "complex.field",
+					Expressions: []string{"expr1", "expr2"},
+					Path:        "complex.field",
 				},
 				{
 					Expressions:          []string{"nested.value"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "complex.nested.inner",
 					StandaloneExpression: true,
 				},
 				{
 					Expressions:          []string{"expr4"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "complex.array[1]",
 					StandaloneExpression: true,
 				},
 				{
 					Expressions:          []string{"expr5"},
-					ExpectedTypes:        []string{"any"},
 					Path:                 "complex.array[2]",
 					StandaloneExpression: true,
 				},
diff --git a/pkg/graph/schema/conversion_cel_type_test.go b/pkg/graph/schema/conversion_cel_type_test.go
new file mode 100644
index 0000000..597f851
--- /dev/null
+++ b/pkg/graph/schema/conversion_cel_type_test.go
@@ -0,0 +1,290 @@
+// Copyright 2025 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package schema
+
+import (
+	"testing"
+
+	"github.com/google/cel-go/cel"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	apiservercel "k8s.io/apiserver/pkg/cel"
+	"k8s.io/apiserver/pkg/cel/openapi"
+	"k8s.io/kube-openapi/pkg/validation/spec"
+	"k8s.io/utils/ptr"
+)
+
+func TestInferSchemaFromCELType_Primitives(t *testing.T) {
+	tests := []struct {
+		name    string
+		celType *cel.Type
+		want    *extv1.JSONSchemaProps
+	}{
+		{
+			name:    "convert bool",
+			celType: cel.BoolType,
+			want:    &extv1.JSONSchemaProps{Type: "boolean"},
+		},
+		{
+			name:    "convert int",
+			celType: cel.IntType,
+			want:    &extv1.JSONSchemaProps{Type: "integer"},
+		},
+		{
+			name:    "convert uint",
+			celType: cel.UintType,
+			want:    &extv1.JSONSchemaProps{Type: "integer"},
+		},
+		{
+			name:    "convert double",
+			celType: cel.DoubleType,
+			want:    &extv1.JSONSchemaProps{Type: "number"},
+		},
+		{
+			name:    "convert string",
+			celType: cel.StringType,
+			want:    &extv1.JSONSchemaProps{Type: "string"},
+		},
+		{
+			name:    "convert bytes",
+			celType: cel.BytesType,
+			want:    &extv1.JSONSchemaProps{Type: "string", Format: "byte"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := inferSchemaFromCELType(tt.celType, nil)
+			require.NoError(t, err)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestInferSchemaFromCELType_Collections(t *testing.T) {
+	tests := []struct {
+		name    string
+		celType *cel.Type
+		want    *extv1.JSONSchemaProps
+	}{
+		{
+			name:    "list of strings",
+			celType: cel.ListType(cel.StringType),
+			want: &extv1.JSONSchemaProps{
+				Type: "array",
+				Items: &extv1.JSONSchemaPropsOrArray{
+					Schema: &extv1.JSONSchemaProps{Type: "string"},
+				},
+			},
+		},
+		{
+			name:    "list of ints",
+			celType: cel.ListType(cel.IntType),
+			want: &extv1.JSONSchemaProps{
+				Type: "array",
+				Items: &extv1.JSONSchemaPropsOrArray{
+					Schema: &extv1.JSONSchemaProps{Type: "integer"},
+				},
+			},
+		},
+		{
+			name:    "map of string to int",
+			celType: cel.MapType(cel.StringType, cel.IntType),
+			want: &extv1.JSONSchemaProps{
+				Type: "object",
+				AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
+					Schema: &extv1.JSONSchemaProps{Type: "integer"},
+				},
+			},
+		},
+		{
+			name:    "map of string to string",
+			celType: cel.MapType(cel.StringType, cel.StringType),
+			want: &extv1.JSONSchemaProps{
+				Type: "object",
+				AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
+					Schema: &extv1.JSONSchemaProps{Type: "string"},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := inferSchemaFromCELType(tt.celType, nil)
+			require.NoError(t, err)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestGenerateSchemaFromCELTypes_Complex(t *testing.T) {
+	addressFields := map[string]*apiservercel.DeclField{
+		"street": apiservercel.NewDeclField("street", apiservercel.StringType, true, nil, nil),
+		"city":   apiservercel.NewDeclField("city", apiservercel.StringType, true, nil, nil),
+		"zip":    apiservercel.NewDeclField("zip", apiservercel.IntType, false, nil, nil),
+	}
+	addressType := apiservercel.NewObjectType("Address", addressFields)
+
+	userFields := map[string]*apiservercel.DeclField{
+		"name":     apiservercel.NewDeclField("name", apiservercel.StringType, true, nil, nil),
+		"age":      apiservercel.NewDeclField("age", apiservercel.IntType, true, nil, nil),
+		"active":   apiservercel.NewDeclField("active", apiservercel.BoolType, false, nil, nil),
+		"score":    apiservercel.NewDeclField("score", apiservercel.DoubleType, false, nil, nil),
+		"tags":     apiservercel.NewDeclField("tags", apiservercel.NewListType(apiservercel.StringType, -1), false, nil, nil),
+		"metadata": apiservercel.NewDeclField("metadata", apiservercel.NewMapType(apiservercel.StringType, apiservercel.StringType, -1), false, nil, nil),
+		"counts":   apiservercel.NewDeclField("counts", apiservercel.NewMapType(apiservercel.StringType, apiservercel.IntType, -1), false, nil, nil),
+		"address":  apiservercel.NewDeclField("address", addressType, false, nil, nil),
+		"extra":    apiservercel.NewDeclField("extra", apiservercel.DynType, false, nil, nil),
+	}
+	userType := apiservercel.NewObjectType("User", userFields)
+	provider := apiservercel.NewDeclTypeProvider(userType, addressType)
+
+	typeMap := map[string]*cel.Type{
+		"user": userType.CelType(),
+	}
+
+	result, err := GenerateSchemaFromCELTypes(typeMap, provider)
+	require.NoError(t, err)
+	require.NotNil(t, result)
+
+	expected := &extv1.JSONSchemaProps{
+		Type: "object",
+		Properties: map[string]extv1.JSONSchemaProps{
+			"user": {
+				Type: "object",
+				Properties: map[string]extv1.JSONSchemaProps{
+					"name":   {Type: "string"},
+					"age":    {Type: "integer"},
+					"active": {Type: "boolean"},
+					"score":  {Type: "number"},
+					"tags": {
+						Type: "array",
+						Items: &extv1.JSONSchemaPropsOrArray{
+							Schema: &extv1.JSONSchemaProps{Type: "string"},
+						},
+					},
+					"metadata": {
+						Type: "object",
+						AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
+							Schema: &extv1.JSONSchemaProps{Type: "string"},
+						},
+					},
+					"counts": {
+						Type: "object",
+						AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
+							Schema: &extv1.JSONSchemaProps{Type: "integer"},
+						},
+					},
+					"address": {
+						Type: "object",
+						Properties: map[string]extv1.JSONSchemaProps{
+							"street": {Type: "string"},
+							"city":   {Type: "string"},
+							"zip":    {Type: "integer"},
+						},
+					},
+					"extra": {
+						XPreserveUnknownFields: ptr.To(true),
+					},
+				},
+			},
+		},
+	}
+
+	assert.Equal(t, expected, result)
+}
+
+func TestExtractSchemaFromDeclTypeWithCycleDetection(t *testing.T) {
+	tests := []struct {
+		name          string
+		setupDeclType func() *apiservercel.DeclType
+		preVisited    func(*apiservercel.DeclType) map[string]bool
+		wantErr       bool
+		wantErrMsg    string
+		validate      func(*testing.T, *extv1.JSONSchemaProps)
+	}{
+		{
+			name: "no cycle - simple object",
+			setupDeclType: func() *apiservercel.DeclType {
+				schema := &spec.Schema{}
+				schema.SchemaProps.Type = []string{"object"}
+				schema.SchemaProps.Properties = map[string]spec.Schema{
+					"name":  {SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
+					"value": {SchemaProps: spec.SchemaProps{Type: []string{"integer"}}},
+				}
+				return openapi.SchemaDeclType(schema, false).MaybeAssignTypeName("SimpleType")
+			},
+			preVisited: func(*apiservercel.DeclType) map[string]bool {
+				return make(map[string]bool)
+			},
+			wantErr: false,
+			validate: func(t *testing.T, result *extv1.JSONSchemaProps) {
+				assert.Equal(t, "object", result.Type)
+				assert.NotNil(t, result.Properties)
+
+				nameProp, hasName := result.Properties["name"]
+				assert.True(t, hasName)
+				assert.Equal(t, "string", nameProp.Type)
+
+				valueProp, hasValue := result.Properties["value"]
+				assert.True(t, hasValue)
+				assert.Equal(t, "integer", valueProp.Type)
+			},
+		},
+		{
+			name: "cycle detected - error out",
+			setupDeclType: func() *apiservercel.DeclType {
+				schema := &spec.Schema{}
+				schema.SchemaProps.Type = []string{"object"}
+				schema.SchemaProps.Properties = map[string]spec.Schema{
+					"name": {SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
+				}
+				return openapi.SchemaDeclType(schema, false).MaybeAssignTypeName("CyclicType")
+			},
+			preVisited: func(dt *apiservercel.DeclType) map[string]bool {
+				// Pre-mark the type as visited to simulate a cycle
+				visited := make(map[string]bool)
+				typeName := dt.CelType().String()
+				visited[typeName] = true
+				return visited
+			},
+			wantErr:    true,
+			wantErrMsg: "cyclic type reference detected",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			declType := tt.setupDeclType()
+			visited := tt.preVisited(declType)
+
+			result, err := extractSchemaFromDeclTypeWithCycleDetection(declType, visited)
+
+			if tt.wantErr {
+				assert.Error(t, err)
+				assert.Contains(t, err.Error(), tt.wantErrMsg)
+				assert.Nil(t, result)
+			} else {
+				assert.NoError(t, err)
+				assert.NotNil(t, result)
+				if tt.validate != nil {
+					tt.validate(t, result)
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/runtime/resolver/resolver_test.go b/pkg/runtime/resolver/resolver_test.go
index ac6cf3a..6b69ecf 100644
--- a/pkg/runtime/resolver/resolver_test.go
+++ b/pkg/runtime/resolver/resolver_test.go
@@ -663,3 +663,132 @@ func TestResolver(t *testing.T) {
 	assert.Equal(t, summary.ResolvedExpressions, 1)
 	assert.Equal(t, "resolved-done", summary.Results[0].Replaced)
 }
+
+// TestResolveFieldWithEmptyBraces tests the regression where strings.Trim() was
+// incorrectly stripping {} from expressions. This affected ternary CEL expressions
+// that end with empty maps like: condition ? value : {}
+func TestResolveFieldWithEmptyBraces(t *testing.T) {
+	tests := []struct {
+		name     string
+		resource map[string]interface{}
+		data     map[string]interface{}
+		field    variable.FieldDescriptor
+		want     ResolutionResult
+	}{
+		{
+			name: "standalone expression ending with empty braces",
+			resource: map[string]interface{}{
+				"metadata": map[string]interface{}{
+					"annotations": "${includeAnnotations ? annotations : {}}",
+				},
+			},
+			data: map[string]interface{}{
+				"includeAnnotations ? annotations : {}": map[string]interface{}{},
+			},
+			field: variable.FieldDescriptor{
+				Path:                 "metadata.annotations",
+				Expressions:          []string{"includeAnnotations ? annotations : {}"},
+				StandaloneExpression: true,
+			},
+			want: ResolutionResult{
+				Path:     "metadata.annotations",
+				Original: "[includeAnnotations ? annotations : {}]",
+				Resolved: true,
+				Replaced: map[string]interface{}{},
+			},
+		},
+		{
+			name: "complex expression with has() and empty braces",
+			resource: map[string]interface{}{
+				"spec": map[string]interface{}{
+					"config": "${has(schema.config) && includeConfig ? schema.config : {}}",
+				},
+			},
+			data: map[string]interface{}{
+				"has(schema.config) && includeConfig ? schema.config : {}": map[string]interface{}{
+					"key": "value",
+				},
+			},
+			field: variable.FieldDescriptor{
+				Path:                 "spec.config",
+				Expressions:          []string{"has(schema.config) && includeConfig ? schema.config : {}"},
+				StandaloneExpression: true,
+			},
+			want: ResolutionResult{
+				Path:     "spec.config",
+				Original: "[has(schema.config) && includeConfig ? schema.config : {}]",
+				Resolved: true,
+				Replaced: map[string]interface{}{
+					"key": "value",
+				},
+			},
+		},
+		{
+			name: "expression with empty braces on both sides",
+			resource: map[string]interface{}{
+				"data": map[string]interface{}{
+					"field": "${condition ? {} : {}}",
+				},
+			},
+			data: map[string]interface{}{
+				"condition ? {} : {}": map[string]interface{}{},
+			},
+			field: variable.FieldDescriptor{
+				Path:                 "data.field",
+				Expressions:          []string{"condition ? {} : {}"},
+				StandaloneExpression: true,
+			},
+			want: ResolutionResult{
+				Path:     "data.field",
+				Original: "[condition ? {} : {}]",
+				Resolved: true,
+				Replaced: map[string]interface{}{},
+			},
+		},
+		{
+			name: "string template with expression ending in braces",
+			resource: map[string]interface{}{
+				"spec": map[string]interface{}{
+					"value": "prefix-${expr ? value : {}}-suffix",
+				},
+			},
+			data: map[string]interface{}{
+				"expr ? value : {}": "resolved",
+			},
+			field: variable.FieldDescriptor{
+				Path:        "spec.value",
+				Expressions: []string{"expr ? value : {}"},
+			},
+			want: ResolutionResult{
+				Path:     "spec.value",
+				Original: "[expr ? value : {}]",
+				Resolved: true,
+				Replaced: "prefix-resolved-suffix",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := NewResolver(tt.resource, tt.data)
+			got := r.resolveField(tt.field)
+
+			assert.Equal(t, tt.want.Path, got.Path)
+			assert.Equal(t, tt.want.Original, got.Original)
+			assert.Equal(t, tt.want.Resolved, got.Resolved)
+			assert.Equal(t, tt.want.Replaced, got.Replaced)
+
+			if tt.want.Error != nil {
+				assert.EqualError(t, got.Error, tt.want.Error.Error())
+			} else {
+				assert.NoError(t, got.Error)
+			}
+
+			if tt.want.Resolved {
+				value, err := r.getValueFromPath(tt.field.Path)
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want.Replaced, value)
+			}
+		})
+	}
+}
diff --git a/test/e2e/chainsaw/check-validation-markers/configmap-assert.yaml b/test/e2e/chainsaw/check-validation-markers/configmap-assert.yaml
index ae6dc15..f4c5ecf 100644
--- a/test/e2e/chainsaw/check-validation-markers/configmap-assert.yaml
+++ b/test/e2e/chainsaw/check-validation-markers/configmap-assert.yaml
@@ -7,5 +7,5 @@ data:
     email: user@example.com
     username: testuser123
     description: A test application
-    tags: [production frontend webapp]
+    tags: production,frontend,webapp
     replicas: 3
diff --git a/test/e2e/chainsaw/check-validation-markers/rgd-assert.yaml b/test/e2e/chainsaw/check-validation-markers/rgd-assert.yaml
index 39c8a28..e3bf993 100644
--- a/test/e2e/chainsaw/check-validation-markers/rgd-assert.yaml
+++ b/test/e2e/chainsaw/check-validation-markers/rgd-assert.yaml
@@ -25,8 +25,8 @@ spec:
             email: ${schema.spec.email}
             username: ${schema.spec.username}
             description: ${schema.spec.description}
-            tags: ${schema.spec.tags}
-            replicas: ${schema.spec.replicas}
+            tags: ${schema.spec.tags.join(",")}
+            replicas: ${string(schema.spec.replicas)}
 status:
   (conditions[?type == 'Ready']):
     - status: 'True'
diff --git a/test/e2e/chainsaw/check-validation-markers/rgd.yaml b/test/e2e/chainsaw/check-validation-markers/rgd.yaml
index 9c12b4a..28c8b9b 100644
--- a/test/e2e/chainsaw/check-validation-markers/rgd.yaml
+++ b/test/e2e/chainsaw/check-validation-markers/rgd.yaml
@@ -36,5 +36,5 @@ spec:
             email: ${schema.spec.email}
             username: ${schema.spec.username}
             description: ${schema.spec.description}
-            tags: ${schema.spec.tags}
-            replicas: ${schema.spec.replicas}
\ No newline at end of file
+            tags: ${schema.spec.tags.join(",")}
+            replicas: ${string(schema.spec.replicas)}
diff --git a/test/integration/environment/setup.go b/test/integration/environment/setup.go
index ab01e9b..e1aa983 100644
--- a/test/integration/environment/setup.go
+++ b/test/integration/environment/setup.go
@@ -140,18 +140,32 @@ func (e *Environment) initializeClients() error {
 }
 
 func (e *Environment) setupController() error {
+	var err error
+	e.CtrlManager, err = ctrl.NewManager(e.ClientSet.RESTConfig(), ctrl.Options{
+		Scheme: scheme.Scheme,
+		Metrics: server.Options{
+			// Disable the metrics server
+			BindAddress: "0",
+		},
+		GracefulShutdownTimeout: ptr.To(30 * time.Second),
+	})
+	if err != nil {
+		return fmt.Errorf("creating manager: %w", err)
+	}
+	e.ClientSet.SetRESTMapper(e.CtrlManager.GetRESTMapper())
+
 	dc := dynamiccontroller.NewDynamicController(
 		zap.New(zap.WriteTo(e.ControllerConfig.LogWriter), zap.UseDevMode(true)),
 		dynamiccontroller.Config{
 			Workers:         3,
-			ResyncPeriod:    60 * time.Second,
+			ResyncPeriod:    0, // disabled resync
 			QueueMaxRetries: 20,
 			MinRetryDelay:   200 * time.Millisecond,
 			MaxRetryDelay:   1000 * time.Second,
 			RateLimit:       10,
 			BurstLimit:      100,
 		},
-		e.ClientSet.Dynamic())
+		e.ClientSet.Metadata(), e.ClientSet.RESTMapper())
 
 	rgReconciler := ctrlresourcegraphdefinition.NewResourceGraphDefinitionReconciler(
 		e.ClientSet,
@@ -161,19 +175,6 @@ func (e *Environment) setupController() error {
 		1,
 	)
 
-	var err error
-	e.CtrlManager, err = ctrl.NewManager(e.ClientSet.RESTConfig(), ctrl.Options{
-		Scheme: scheme.Scheme,
-		Metrics: server.Options{
-			// Disable the metrics server
-			BindAddress: "0",
-		},
-		GracefulShutdownTimeout: ptr.To(30 * time.Second),
-	})
-	if err != nil {
-		return fmt.Errorf("creating manager: %w", err)
-	}
-
 	if err := e.CtrlManager.Add(dc); err != nil {
 		return fmt.Errorf("adding dynamic controller to manager: %w", err)
 	}
diff --git a/test/integration/suites/core/crd_test.go b/test/integration/suites/core/crd_test.go
index 6ac5ec1..b46acb2 100644
--- a/test/integration/suites/core/crd_test.go
+++ b/test/integration/suites/core/crd_test.go
@@ -75,7 +75,7 @@ var _ = Describe("CRD", func() {
 					},
 					"data": map[string]interface{}{
 						"key":  "value",
-						"key2": "${schema.spec.field2}",
+						"key2": "${string(schema.spec.field2)}",
 					},
 				}, nil, nil),
 			)
diff --git a/test/integration/suites/core/dependency_readiness_test.go b/test/integration/suites/core/dependency_readiness_test.go
index eb0d596..162fe01 100644
--- a/test/integration/suites/core/dependency_readiness_test.go
+++ b/test/integration/suites/core/dependency_readiness_test.go
@@ -21,6 +21,7 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	appsv1 "k8s.io/api/apps/v1"
+	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -372,4 +373,144 @@ var _ = Describe("Dependency Readiness", func() {
 			g.Expect(err).To(MatchError(errors.IsNotFound, "rgd should be deleted"))
 		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
 	})
+
+	It("should block dependent resources until readyWhen conditions are satisfied", func(ctx SpecContext) {
+		instanceName := "test-jobs-instance"
+		rgd := generator.NewResourceGraphDefinition("test-jobs",
+			generator.WithSchema(
+				"TestJobs", "v1alpha1",
+				map[string]interface{}{
+					"name": "string",
+				},
+				nil,
+			),
+			generator.WithResource("job1", map[string]interface{}{
+				"apiVersion": "batch/v1",
+				"kind":       "Job",
+				"metadata": map[string]interface{}{
+					"name": "${schema.spec.name}-job1",
+				},
+				"spec": map[string]interface{}{
+					"template": map[string]interface{}{
+						"spec": map[string]interface{}{
+							"containers": []interface{}{
+								map[string]interface{}{
+									"name":    "sleeper",
+									"image":   "busybox",
+									"command": []interface{}{"sh", "-c", "echo 'Job 1 starting' && sleep 5 && echo 'Job 1 complete'"},
+								},
+							},
+							"restartPolicy": "Never",
+						},
+					},
+				},
+			}, []string{"${job1.status.?completionTime.orValue(null) != null}"}, nil),
+			generator.WithResource("job2", map[string]interface{}{
+				"apiVersion": "batch/v1",
+				"kind":       "Job",
+				"metadata": map[string]interface{}{
+					"name": "${schema.spec.name}-job2",
+					"annotations": map[string]interface{}{
+						"depends-on": "${job1.metadata.name}",
+					},
+				},
+				"spec": map[string]interface{}{
+					"template": map[string]interface{}{
+						"spec": map[string]interface{}{
+							"containers": []interface{}{
+								map[string]interface{}{
+									"name":    "sleeper",
+									"image":   "busybox",
+									"command": []interface{}{"sh", "-c", "echo 'Job 2 starting' && sleep 1 && echo 'Job 2 complete'"},
+								},
+							},
+							"restartPolicy": "Never",
+						},
+					},
+				},
+			}, []string{"${job2.status.?completionTime.orValue(null) != null}"}, nil),
+		)
+
+		Expect(env.Client.Create(ctx, rgd)).To(Succeed())
+
+		createdRGD := &krov1alpha1.ResourceGraphDefinition{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{Name: rgd.Name}, createdRGD)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			var readyCondition krov1alpha1.Condition
+			for _, cond := range createdRGD.Status.Conditions {
+				if cond.Type == resourcegraphdefinition.Ready {
+					readyCondition = cond
+				}
+			}
+			g.Expect(readyCondition).ToNot(BeNil())
+			g.Expect(readyCondition.Status).To(Equal(metav1.ConditionTrue))
+		}, 30*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		instance := &unstructured.Unstructured{
+			Object: map[string]interface{}{
+				"apiVersion": fmt.Sprintf("%s/%s", krov1alpha1.KRODomainName, "v1alpha1"),
+				"kind":       "TestJobs",
+				"metadata": map[string]interface{}{
+					"name":      instanceName,
+					"namespace": namespace,
+				},
+				"spec": map[string]interface{}{
+					"name": "test-jobs",
+				},
+			},
+		}
+
+		Expect(env.Client.Create(ctx, instance)).To(Succeed())
+
+		job1 := &batchv1.Job{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      "test-jobs-job1",
+				Namespace: namespace,
+			}, job1)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 30*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		Consistently(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      "test-jobs-job2",
+				Namespace: namespace,
+			}, &batchv1.Job{})
+			g.Expect(err).To(MatchError(errors.IsNotFound, "job2 should not be created while job1 is running"))
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		now := metav1.Now()
+		job1.Status.CompletionTime = &now
+		job1.Status.Succeeded = 1
+		Expect(env.Client.Status().Update(ctx, job1)).To(Succeed())
+
+		job2 := &batchv1.Job{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      "test-jobs-job2",
+				Namespace: namespace,
+			}, job2)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 30*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		now = metav1.Now()
+		job2.Status.CompletionTime = &now
+		job2.Status.Succeeded = 1
+		Expect(env.Client.Status().Update(ctx, job2)).To(Succeed())
+
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			state, found, err := unstructured.NestedString(instance.Object, "status", "state")
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(found).To(BeTrue())
+			g.Expect(state).To(Equal("ACTIVE"))
+		}, 30*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+	}, SpecTimeout(120*time.Second))
 })
diff --git a/test/integration/suites/core/instance_resource_watch_test.go b/test/integration/suites/core/instance_resource_watch_test.go
new file mode 100644
index 0000000..b21ff55
--- /dev/null
+++ b/test/integration/suites/core/instance_resource_watch_test.go
@@ -0,0 +1,118 @@
+// Copyright 2025 The Kube Resource Orchestrator Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package core_test
+
+import (
+	"fmt"
+	"time"
+
+	krov1alpha1 "github.com/kubernetes-sigs/kro/api/v1alpha1"
+	"github.com/kubernetes-sigs/kro/pkg/testutil/generator"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/rand"
+)
+
+var _ = Describe("Instance Resource Watch", func() {
+	var namespace string
+
+	BeforeEach(func(ctx SpecContext) {
+		namespace = fmt.Sprintf("test-%s", rand.String(5))
+		Expect(env.Client.Create(ctx, &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: namespace,
+			},
+		})).To(Succeed())
+	})
+
+	It("watch behavior on Secret", func(ctx SpecContext) {
+		rgd := generator.NewResourceGraphDefinition("test-instance-resource-reconcile-reactive",
+			generator.WithSchema(
+				"TestResourceWatch", "v1alpha1",
+				map[string]interface{}{
+					"field1": "string",
+				},
+				nil,
+			),
+			generator.WithResource("res1", map[string]interface{}{
+				"apiVersion": "v1",
+				"kind":       "ConfigMap",
+				"metadata": map[string]interface{}{
+					"name": "static-configmap",
+				},
+				"data": map[string]interface{}{
+					"key": "${schema.spec.field1}",
+				},
+			}, nil, nil),
+		)
+
+		Expect(env.Client.Create(ctx, rgd)).To(Succeed())
+
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{Name: rgd.Name}, rgd)
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(rgd.Status.Conditions).To(Not(BeNil()))
+			g.Expect(rgd.Status.State).To(Equal(krov1alpha1.ResourceGraphDefinitionStateActive))
+			for _, cond := range rgd.Status.Conditions {
+				g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
+			}
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		instance := &unstructured.Unstructured{}
+		instance.SetAPIVersion(krov1alpha1.GroupVersion.String())
+		instance.SetKind("TestResourceWatch")
+		instance.SetName("test-instance")
+		instance.SetNamespace(namespace)
+		instance.Object["spec"] = map[string]interface{}{
+			"field1": "foo",
+		}
+		Expect(env.Client.Create(ctx, instance)).To(Succeed())
+
+		Eventually(func(g Gomega, ctx SpecContext) {
+			g.Expect(env.Client.Get(ctx, types.NamespacedName{
+				Name:      instance.GetName(),
+				Namespace: instance.GetNamespace(),
+			}, instance)).ToNot(HaveOccurred())
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		cfgMap := &corev1.ConfigMap{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			g.Expect(env.Client.Get(ctx, types.NamespacedName{
+				Name:      "static-configmap",
+				Namespace: namespace,
+			}, cfgMap)).ToNot(HaveOccurred())
+			g.Expect(cfgMap.Data).To(HaveKeyWithValue("key", "foo"))
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		time.Sleep(5 * time.Second)
+		cfgMap.Data["key"] = "updated"
+		Expect(env.Client.Update(ctx, cfgMap)).To(Succeed())
+
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      "static-configmap",
+				Namespace: namespace,
+			}, cfgMap)
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(cfgMap.Data).To(HaveKeyWithValue("key", "updated"))
+		}, 3*time.Second, time.Second).WithContext(ctx).Should(Succeed(),
+			"ConfigMap should be updated to reflect the instance reconcile due to watch on resources",
+		)
+	})
+})
diff --git a/test/integration/suites/core/readiness_test.go b/test/integration/suites/core/readiness_test.go
index 51ff2fc..8d84d58 100644
--- a/test/integration/suites/core/readiness_test.go
+++ b/test/integration/suites/core/readiness_test.go
@@ -68,15 +68,11 @@ var _ = Describe("Readiness", func() {
 					"replicas": "integer",
 					"deployment": map[string]interface{}{
 						"includeAnnotations": "boolean | default=false",
-						"annotations": map[string]interface{}{
-							"app": "string | default=nginx",
-						},
+						"annotations":        "map[string]string",
 					},
 					"service": map[string]interface{}{
 						"includeAnnotations": "boolean | default=true",
-						"annotations": map[string]interface{}{
-							"app": "string | default=service",
-						},
+						"annotations":        "map[string]string",
 					},
 				},
 				nil,
@@ -86,9 +82,8 @@ var _ = Describe("Readiness", func() {
 				"apiVersion": "apps/v1",
 				"kind":       "Deployment",
 				"metadata": map[string]interface{}{
-					"name": "${schema.spec.name}",
-					"annotations": `${schema.spec.deployment.includeAnnotations == true
-								? schema.spec.deployment.annotations : null}`,
+					"name":        "${schema.spec.name}",
+					"annotations": `${schema.spec.deployment.includeAnnotations ? schema.spec.deployment.annotations : {}}`,
 				},
 				"spec": map[string]interface{}{
 					"replicas": "${schema.spec.replicas}",
@@ -124,9 +119,8 @@ var _ = Describe("Readiness", func() {
 				"apiVersion": "v1",
 				"kind":       "Service",
 				"metadata": map[string]interface{}{
-					"name": "${deployment.metadata.name}",
-					"annotations": `${schema.spec.service.includeAnnotations == true
-								? schema.spec.service.annotations : null}`,
+					"name":        "${deployment.metadata.name}",
+					"annotations": `${schema.spec.service.includeAnnotations ? schema.spec.service.annotations : {}}`,
 				},
 				"spec": map[string]interface{}{
 					"selector": map[string]interface{}{