Repository: kubernetes-sigs/kro

Issue:
Regression: readyWhen condition is ignored when orchestrating dependencies
### Description

**Observed Behavior**:
Job 2 is created immediately after job 1, without waiting for job 1 to satisfy its `readyWhen` condition.

**Expected Behavior**:
Job 2 is created after job 1 becomes ready according to its `readyWhen` condition.

**Reproduction Steps** :

RGD:
```yaml
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: test-jobs
spec:
  schema:
    apiVersion: v1
    kind: TestJobs
  resources:
    - id: job1
      readyWhen:
        - ${job1.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-1
          namespace: kro-tests
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: ["sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #1'"]
              restartPolicy: Never
    - id: job2
      readyWhen:
        - ${job2.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-2
          namespace: ${job1.metadata.namespace}
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: [ "sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #1'" ]
              restartPolicy: Never
```

RGD instance:
```yaml
apiVersion: kro.run/v1
kind: TestJobs
metadata:
  name: test-jobs
  namespace: kro-tests
```

**Versions**:
- kro version: 0.5.0
- Kubernetes Version (`kubectl version`): 
```
Client Version: v1.34.1
Kustomize Version: v5.7.1
Server Version: v1.34.0
```

**Logs**:
```
2025-10-22T17:02:44.096Z    DEBUG    dynamic-controller    Enqueueing object    {"objectIdentifiers": {"NamespacedKey":"kro-tests/test-jobs","GVR":{"Group":"kro.run","Version":"v1","Resource":"testjobs"}}, "eventType": "add"}
2025-10-22T17:02:44.096Z    DEBUG    dynamic-controller    Syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs"}
2025-10-22T17:02:44.097Z    DEBUG    testjobs-controller    Setting managed state    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "name": "test-jobs", "namespace": "kro-tests"}
2025-10-22T17:02:44.099Z    LEVEL(-2)    dynamic-controller    Skipping update due to unchanged generation    {"name": "test-jobs", "namespace": "kro-tests", "generation": 1}
2025-10-22T17:02:44.100Z    LEVEL(-2)    testjobs-controller    adding object to applyset    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-1]", "cluster-revision": ""}
2025-10-22T17:02:44.103Z    LEVEL(-2)    testjobs-controller    applied object    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-1]", "applied-revision": "4036", "error": null}
2025-10-22T17:02:44.103Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "resource job1 is not resolved", "error": null}
2025-10-22T17:02:44.106Z    LEVEL(-2)    dynamic-controller    Skipping update due to unchanged generation    {"name": "test-jobs", "namespace": "kro-tests", "generation": 1}
2025-10-22T17:02:44.106Z    DEBUG    dynamic-controller    Finished syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs", "duration": "9.749541ms"}
2025-10-22T17:02:44.106Z    DEBUG    dynamic-controller    Requeue needed after delay    {"item": {"NamespacedKey":"kro-tests/test-jobs","GVR":{"Group":"kro.run","Version":"v1","Resource":"testjobs"}}, "error": "unresolved resource: job2", "delay": "3s"}
2025-10-22T17:02:47.107Z    DEBUG    dynamic-controller    Syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs"}
2025-10-22T17:02:47.117Z    LEVEL(-2)    testjobs-controller    adding object to applyset    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-1]", "cluster-revision": "4039"}
2025-10-22T17:02:47.120Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "", "error": "failed evaluating expressison job1.status.completionTime != null: failed evaluating expression job1.status.completionTime != null: no such key: completionTime"}
2025-10-22T17:02:47.123Z    LEVEL(-2)    testjobs-controller    adding object to applyset    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-2]", "cluster-revision": ""}
2025-10-22T17:02:47.127Z    LEVEL(-2)    testjobs-controller    applied object    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-2]", "applied-revision": "4053", "error": null}
2025-10-22T17:02:47.127Z    LEVEL(-2)    testjobs-controller    applied object    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-1]", "applied-revision": "4039", "error": null}
2025-10-22T17:02:47.127Z    LEVEL(-2)    testjobs-controller    listing objects for pruning    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "namespace": "kro-tests", "resource": "batch/v1, Resource=jobs"}
2025-10-22T17:02:47.129Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job2", "reason": "resource job2 is not resolved", "error": null}
2025-10-22T17:02:47.129Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "", "error": "failed evaluating expressison job1.status.completionTime != null: failed evaluating expression job1.status.completionTime != null: no such key: completionTime"}
2025-10-22T17:02:47.134Z    DEBUG    dynamic-controller    Finished syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs", "duration": "27.101292ms"}
2025-10-22T17:02:47.134Z    DEBUG    dynamic-controller    Requeue needed after delay    {"item": {"NamespacedKey":"kro-tests/test-jobs","GVR":{"Group":"kro.run","Version":"v1","Resource":"testjobs"}}, "error": "changes applied to cluster", "delay": "3s"}
2025-10-22T17:02:47.134Z    LEVEL(-2)    dynamic-controller    Skipping update due to unchanged generation    {"name": "test-jobs", "namespace": "kro-tests", "generation": 1}
2025-10-22T17:02:50.135Z    DEBUG    dynamic-controller    Syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs"}
2025-10-22T17:02:50.154Z    LEVEL(-2)    testjobs-controller    adding object to applyset    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-1]", "cluster-revision": "4063"}
2025-10-22T17:02:50.154Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "", "error": "failed evaluating expressison job1.status.completionTime != null: failed evaluating expression job1.status.completionTime != null: no such key: completionTime"}
2025-10-22T17:02:50.158Z    LEVEL(-2)    testjobs-controller    adding object to applyset    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-2]", "cluster-revision": "4071"}
2025-10-22T17:02:50.159Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job2", "reason": "", "error": "failed evaluating expressison job2.status.completionTime != null: failed evaluating expression job2.status.completionTime != null: no such key: completionTime"}
2025-10-22T17:02:50.164Z    LEVEL(-2)    testjobs-controller    applied object    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-2]", "applied-revision": "4071", "error": null}
2025-10-22T17:02:50.165Z    LEVEL(-2)    testjobs-controller    applied object    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "object": "[batch/v1, Kind=Job:kro-tests/job-1]", "applied-revision": "4063", "error": null}
2025-10-22T17:02:50.165Z    LEVEL(-2)    testjobs-controller    listing objects for pruning    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "namespace": "kro-tests", "resource": "batch/v1, Resource=jobs"}
2025-10-22T17:02:50.169Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job2", "reason": "", "error": "failed evaluating expressison job2.status.completionTime != null: failed evaluating expression job2.status.completionTime != null: no such key: completionTime"}
2025-10-22T17:02:50.169Z    DEBUG    testjobs-controller    Resource not ready    {"controller": "testjobs", "controllerGroup": "kro.run", "controllerKind": "TestJobs", "namespace": "kro-tests", "name": "test-jobs", "resourceID": "job1", "reason": "", "error": "failed evaluating expressison job1.status.completionTime != null: failed evaluating expression job1.status.completionTime != null: no such key: completionTime"}
2025-10-22T17:02:50.182Z    DEBUG    dynamic-controller    Finished syncing object    {"gvr": "kro.run/v1/testjobs", "namespacedKey": "kro-tests/test-jobs", "duration": "47.002583ms"}
2025-10-22T17:02:50.182Z    LEVEL(-2)    dynamic-controller    Skipping update due to unchanged generation    {"name": "test-jobs", "namespace": "kro-tests", "generation": 1}
```

* 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 to help the community and maintainers prioritize this request
* Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
* If you are interested in working on this issue or have submitted a pull request, please leave a comment

### Which option describes the most your issue?

_No response_

Discussion:
@govindup63: I would like to work on this issue @jakobmoellerdev: I dont know about this. Please check
https://kro.run/docs/concepts/resource-group-definitions#more-about-resources

In here we state

```yaml
spec:
  resources:
    - id: my-resource
      template || externalRef: {} # users can either template resources or reference objects outside the graph
      readyWhen:
      # users can specify CEL expressions to determine when a resource is ready
      - ${deployment.status.conditions.exists(x, x.type == 'Available' && x.status == "True")}
      includeWhen:
      # users can specify CEL expressions to determine when a resource should be included in the graph
      - ${schema.spec.value.enabled}
```

as per this specification, i believe this is intended behavior and if 0.4.1 behaved, this would have been incorrect before.

We need to now do a judgement call if readyWhen implies includeWhen or not ( IMHO I think it does not )@devdanylo: > includeWhen

Hi @jakobmoellerdev,
Why are you referring to `includeWhen`?@jakobmoellerdev: because of 

> Expected Behavior:
  Job 2 is created after job 1 becomes ready according to its readyWhen condition.

if you want job 2 to be created after job 1 then you should use includeWhen because that will include the job only in the graph when the condition is fullfilled. readyWhen is only a resource readiness condition I believe.@devdanylo: How about the implicit dependency between the jobs, expressed via the metadata of the 2nd job:
```yaml
metadata:
          name: job-2
          namespace: ${job1.metadata.namespace}
```
@jakobmoellerdev: that namespace is available immediately so while job1 is applied before job2, there is no need to wait for this object to become ready before applying it.@devdanylo: Okay, I see.

Based on the documentation and examples, it appears that the `includeWhen` condition is evaluated only once during instance creation and is primarily intended to determine which resources to install according to the user's configuration. It does not seem to be designed for orchestrating the installation order of resources.

If my understanding above of `includeWhen` is correct, and `readyWhen` does not affect cases where one resource references values from another, the question becomes how to enforce installation order based on readiness. Is the expected behavior that resources are installed following topological order and readiness?@devdanylo: @jakobmoellerdev could you please take another look into this issue? With KRO v0.5.x it's now not clear how to orchestrate resource installation order based on resource readiness.

I've tried playing around with `includeWhen`, but it fails when the controller attempts to reconcile the RGD:
```
failed to validate resource CEL expressions: failed to ensure resource job2 includeWhen expressions: failed to dry-run expression job1.status.completionTime != null: failed to dry-run expression job1.status.completionTime != null: failed to evaluate expression: no such attribute(s): job1
```

RGD:
```yaml
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: test-jobs
spec:
  schema:
    apiVersion: v1
    kind: TestJobs
  resources:
    - id: job1
      readyWhen:
        - ${job1.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-1
          namespace: kro-tests
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: ["sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #1'"]
              restartPolicy: Never
    - id: job2
      readyWhen:
        - ${job2.status.completionTime != null}
      includeWhen:
        - ${job1.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-2
          namespace: kro-tests
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: [ "sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #2'" ]
              restartPolicy: Never
```

cc @a-hilaly @a-hilaly: @devdanylo please see https://github.com/kubernetes-sigs/kro/pull/756#discussion_r2464472698

In your situation you don't need includeWhen, job2 will be created after job1 is completed. The thing is that kro will only move to the next resources in topological order when previous resources all reach ready state.@devdanylo: @a-hilaly 

> In your situation you don't need includeWhen, job2 will be created after job1 is completed. The thing is that kro will only move to the next resources in topological order when previous resources all reach ready state.

Exactly, that's my expectation too! But that's not how it work in 0.5.1. You can reproduce it with this RGD (both jobs are created at the same time):
```yaml
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: test-jobs
spec:
  schema:
    apiVersion: v1
    kind: TestJobs
  resources:
    - id: job1
      readyWhen:
        - ${job1.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-1
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: ["sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #1'"]
              restartPolicy: Never
    - id: job2
      readyWhen:
        - ${job2.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-2
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: [ "sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #2'" ]
              restartPolicy: Never
```@jakobmoellerdev: alrighty sounds like I simply had a wrong expectation then. will supply a fix tomorrow. Thanks for sticking with us in this case!@jakobmoellerdev: /assign @govindup63

> I would like to work on this issue

actually let me correct that, let me know if you need help!
@a-hilaly: @govindup63 this is a very critical issue and needs a fix ASAP, will you able to send a fix soon?@govindup63: Absolutely, I’ll get on it and push a fix soon. I’ll keep this thread posted on the progress.@a-hilaly: @govindup63 it would be great to understand why didn't this test catch these regressions https://github.com/kubernetes-sigs/kro/blob/main/test/integration/suites/core/readiness_test.go@devdanylo: Hi @govindup63,
Is there any update on this issue?
We’ve been waiting for some critical fixes included in v0.5.0, but this bug is currently blocking us from adopting v0.5.x.
Thank you in advance.@govindup63: I have made changes in the code...just trying to test this on remote cluster @devdanylo: @govindup63 here is a minikube setup I was using for reproducing the issue:
```sh
minikube start -p kro-poc --driver=docker --memory 4096 --cpus 2

minikube update-context -p kro-poc

kubectl apply --server-side -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: kro
EOF

kubectl apply --server-side -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: kro-tests
EOF

helm template kro oci://registry.k8s.io/kro/charts/kro \
  --namespace kro \
  --version=v0.5.0 \
  --set image.tag=v0.5.1 \
  --include-crds | kubectl apply --namespace kro --server-side -f -
```@a-hilaly: @govindup63 this issue is critical urgent. any updates on this? You can open the PR first to kick off the review and see e2e test results. @anthonyh-quilt: Hello, I discovered this issue when attempting to resolve a race condition with kargo resource creation. I wanted to intuitively use readyWhen but remembered what @jakobmoellerdev had mentioned [here](https://github.com/kubernetes-sigs/kro/issues/744#issuecomment-3437488235)

I _think_ my usecase is resolved with each resource getting created in order and not all at once... 

A kargo project requires all resources to be contained within a namespace that matches the name of a Project- when the Project is created, kargo will create+associate the namespace (I don't know if this is asynchronous)

Point being- there are some edge cases with regards to orchestrating rgd insanitation so I wanted to +1 this bug and share a [gist](https://gist.github.com/anthonyh-quilt/ddf5f4c1ce4ce7a7a6c4f055aeaeb8f6) of my usecase to help feed the brain trust. Thank you 👍 @tjamet: If I read correctly, this issue suggests following the resource order to deploy resources.

I wonder if sticking with the strict list order is the right thing to do. It sounds like breaking the idea of a graph.

I read `resources` more like a dictionary of resources (similar to k8s's containers), stored as list to ensure idempotence when serializing jsons.

Enforcing deploying resources in the order they appear in the list sounds counter productive. (and against [Kro's RGD definition](https://kro.run/docs/getting-started/deploy-a-resource-graph-definition) "detects the correct deployment order")
It pushes burden on the users to resolve an order in which resources shall be deployed and reduces the interest of having a `graph`

let's imagine there is a RGD similar (in concept) to this

```
resources:
- id: preparation-job
  readyWhen: ${preparation-job.status.completedAt != null}
  template:
    spec:
      template:
        spec:
          containers:
            - name: some-job
- id: config-map
   template:
     metadata:
        name: some-cm
     data:
        some-key: some-value
- id: long-term-deployment
  template:
    spec:
      template:
        spec:
          containers:
          - name: some-task
            envFrom:
              configMapRef:
                name: some-cm
```

It works great, gets deployed  successfully with KRO. Now, there is an evolution where the preparation-job also needs the configmap.
What should happen?
Shall the maintainer of the RGD spot the dependency, solve the dependency graph and re-order the resources?

If KRO sticks to the resources order, then, yes, you need to re-order the resources and you push a burden on users they shouldn't care about IMO.

in the example

```
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: test-jobs
spec:
  schema:
    apiVersion: v1
    kind: TestJobs
  resources:
    - id: job1
      readyWhen:
        - ${job1.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-1
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: ["sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #1'"]
              restartPolicy: Never
    - id: job2
      readyWhen:
        - ${job2.status.completionTime != null}
      template:
        apiVersion: batch/v1
        kind: Job
        metadata:
          name: job-2
        spec:
          template:
            spec:
              containers:
                - name: sleeper
                  image: busybox
                  command: [ "sh", "-c", "echo 'Sleeping for 30 seconds' && sleep 30 && echo 'Hello from the Kubernetes Job #2'" ]
              restartPolicy: Never
```

It would be interesting to better understand the use case why job1 *must* complete before *job2* gets deployed and starts.
In this graph, there is no explicit dependency; I would personally expect all resources to be deployed together.@anthonyh-quilt: I agree @tjamet with the scenario you've presented

In my use case job1 results in a new namespace being created (the namespace is created by an external controller) 
The rest of the jobs deploy to this new namespace (the name of the namespace == the name of job1's resource)

So, the status of the RGD was stuck IN_PROGRESS with a number of issues being listed "namespace does not exist"

I will keep testing this out to better understand things but it's an edge case for sure.@tjamet: thanks for sharing, it helps a lot thinking about the problem 

Solution patch:
diff --git a/NOTICE b/NOTICE
index ba72306..b90cca4 100644
--- a/NOTICE
+++ b/NOTICE
@@ -16,7 +16,6 @@ Submitted on behalf of a third-party: @candonov
 Submitted on behalf of a third-party: @chkp-dvirpa
 Submitted on behalf of a third-party: @codesenju
 Submitted on behalf of a third-party: @feliperubo
-Submitted on behalf of a third-party: @gfrey
 Submitted on behalf of a third-party: @HeeManSu7
 Submitted on behalf of a third-party: @iamahgoub
 Submitted on behalf of a third-party: @jknutson
diff --git a/pkg/controller/instance/controller_reconcile.go b/pkg/controller/instance/controller_reconcile.go
index c12fd19..0ccf674 100644
--- a/pkg/controller/instance/controller_reconcile.go
+++ b/pkg/controller/instance/controller_reconcile.go
@@ -142,6 +142,25 @@ func (igr *instanceGraphReconciler) updateResourceReadiness(resourceID string) {
 	}
 }
 
+// areDependenciesReady checks if all dependencies of a resource are ready
+func (igr *instanceGraphReconciler) areDependenciesReady(resourceID string) bool {
+	dependencies := igr.runtime.ResourceDescriptor(resourceID).GetDependencies()
+
+	for _, depID := range dependencies {
+		// Check if dependency is resolved
+		if _, state := igr.runtime.GetResource(depID); state != runtime.ResourceStateResolved {
+			return false
+		}
+
+		// Check if dependency satisfies its readyWhen conditions
+		if ready, _, err := igr.runtime.IsResourceReady(depID); err != nil || !ready {
+			return false
+		}
+	}
+
+	return true
+}
+
 // reconcileInstance handles the reconciliation of an active instance
 func (igr *instanceGraphReconciler) reconcileInstance(ctx context.Context) error {
 	instance := igr.runtime.GetInstance()
@@ -199,6 +218,13 @@ func (igr *instanceGraphReconciler) reconcileInstance(ctx context.Context) error
 			break
 		}
 
+		// Check if all dependencies are ready
+		if !igr.areDependenciesReady(resourceID) {
+			unresolvedResourceID = resourceID
+			prune = false
+			break
+		}
+
 		applyable := applyset.ApplyableObject{
 			Unstructured: resource,
 			ID:           resourceID,
@@ -212,7 +238,7 @@ func (igr *instanceGraphReconciler) reconcileInstance(ctx context.Context) error
 		if clusterObj != nil {
 			igr.runtime.SetResource(resourceID, clusterObj)
 			igr.updateResourceReadiness(resourceID)
-			// Synchronize runtime state after each resource
+			// Synchronize runtime state after each resource to re-evaluate CEL expressions
 			if _, err := igr.runtime.Synchronize(); err != nil {
 				return fmt.Errorf("failed to synchronize after apply/prune: %w", err)
 			}
@@ -226,6 +252,10 @@ func (igr *instanceGraphReconciler) reconcileInstance(ctx context.Context) error
 			resourceState.State = ResourceStateError
 			resourceState.Err = applied.Error
 		} else {
+			// Update runtime with the applied resource
+			if applied.LastApplied != nil {
+				igr.runtime.SetResource(applied.ID, applied.LastApplied)
+			}
 			igr.updateResourceReadiness(applied.ID)
 		}
 	}



Test patch:
diff --git a/test/integration/suites/core/dependency_readiness_test.go b/test/integration/suites/core/dependency_readiness_test.go
new file mode 100644
index 0000000..eb0d596
--- /dev/null
+++ b/test/integration/suites/core/dependency_readiness_test.go
@@ -0,0 +1,375 @@
+// 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 core_test
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	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"
+
+	krov1alpha1 "github.com/kubernetes-sigs/kro/api/v1alpha1"
+	"github.com/kubernetes-sigs/kro/pkg/controller/resourcegraphdefinition"
+	"github.com/kubernetes-sigs/kro/pkg/testutil/generator"
+)
+
+var _ = Describe("Dependency Readiness", func() {
+	var (
+		namespace string
+	)
+
+	BeforeEach(func(ctx SpecContext) {
+		namespace = fmt.Sprintf("test-%s", rand.String(5))
+		// Create namespace
+		ns := &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: namespace,
+			},
+		}
+		Expect(env.Client.Create(ctx, ns)).To(Succeed())
+	})
+
+	AfterEach(func(ctx SpecContext) {
+		Expect(env.Client.Delete(ctx, &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: namespace,
+			},
+		})).To(Succeed())
+	})
+
+	It("should wait for all dependencies to be ready before creating dependent resource", func(ctx SpecContext) {
+		// This test creates a resource graph with:
+		// - ConfigMap A (no dependencies, has readyWhen condition)
+		// - ConfigMap B (no dependencies, has readyWhen condition)
+		// - Deployment (depends on both ConfigMap A and B)
+		// The deployment should only be created after both configmaps satisfy their readyWhen conditions
+
+		rgd := generator.NewResourceGraphDefinition("test-dependency-readiness",
+			generator.WithSchema(
+				"TestDependencyReadiness", "v1alpha1",
+				map[string]interface{}{
+					"name": "string",
+					"configA": map[string]interface{}{
+						"data":  "string",
+						"ready": "boolean | default=false",
+					},
+					"configB": map[string]interface{}{
+						"data":  "string",
+						"ready": "boolean | default=false",
+					},
+					"replicas": "integer | default=1",
+				},
+				nil,
+			),
+			// ConfigMap A - no dependencies, has readyWhen
+			generator.WithResource("configmapA", map[string]interface{}{
+				"apiVersion": "v1",
+				"kind":       "ConfigMap",
+				"metadata": map[string]interface{}{
+					"name": "${schema.spec.name}-config-a",
+				},
+				"data": map[string]interface{}{
+					"value": "${schema.spec.configA.data}",
+					"ready": "${string(schema.spec.configA.ready)}",
+				},
+			}, []string{"${configmapA.data.?ready.orValue(\"false\") == \"true\"}"}, nil),
+			// ConfigMap B - no dependencies, has readyWhen
+			generator.WithResource("configmapB", map[string]interface{}{
+				"apiVersion": "v1",
+				"kind":       "ConfigMap",
+				"metadata": map[string]interface{}{
+					"name": "${schema.spec.name}-config-b",
+				},
+				"data": map[string]interface{}{
+					"value": "${schema.spec.configB.data}",
+					"ready": "${string(schema.spec.configB.ready)}",
+				},
+			}, []string{"${configmapB.data.?ready.orValue(\"false\") == \"true\"}"}, nil),
+			// Deployment - depends on both configmaps
+			generator.WithResource("deployment", map[string]interface{}{
+				"apiVersion": "apps/v1",
+				"kind":       "Deployment",
+				"metadata": map[string]interface{}{
+					"name": "${schema.spec.name}",
+				},
+				"spec": map[string]interface{}{
+					"replicas": "${schema.spec.replicas}",
+					"selector": map[string]interface{}{
+						"matchLabels": map[string]interface{}{
+							"app": "test",
+						},
+					},
+					"template": map[string]interface{}{
+						"metadata": map[string]interface{}{
+							"labels": map[string]interface{}{
+								"app": "test",
+							},
+						},
+						"spec": map[string]interface{}{
+							"containers": []interface{}{
+								map[string]interface{}{
+									"name":  "nginx",
+									"image": "nginx",
+									"env": []interface{}{
+										map[string]interface{}{
+											"name":  "CONFIG_A",
+											"value": "${configmapA.data.?value.orValue(\"\")}",
+										},
+										map[string]interface{}{
+											"name":  "CONFIG_B",
+											"value": "${configmapB.data.?value.orValue(\"\")}",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			}, nil, nil),
+		)
+
+		// Create ResourceGraphDefinition
+		Expect(env.Client.Create(ctx, rgd)).To(Succeed())
+
+		// Verify ResourceGraphDefinition is created and becomes ready
+		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())
+
+			// Verify the ResourceGraphDefinition fields
+			g.Expect(createdRGD.Spec.Schema.Kind).To(Equal("TestDependencyReadiness"))
+			g.Expect(createdRGD.Spec.Schema.APIVersion).To(Equal("v1alpha1"))
+			g.Expect(createdRGD.Spec.Resources).To(HaveLen(3))
+
+			// Verify topological order (configmaps should come before deployment)
+			g.Expect(createdRGD.Status.TopologicalOrder).To(HaveLen(3))
+			g.Expect(createdRGD.Status.TopologicalOrder[2]).To(Equal("deployment"))
+
+			// Verify ready condition
+			g.Expect(createdRGD.Status.Conditions).ShouldNot(BeEmpty())
+			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))
+			g.Expect(readyCondition.ObservedGeneration).To(Equal(createdRGD.Generation))
+
+			g.Expect(createdRGD.Status.State).To(Equal(krov1alpha1.ResourceGraphDefinitionStateActive))
+
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		instanceName := "test-dep-readiness"
+		// Create instance with both configmaps NOT ready initially
+		instance := &unstructured.Unstructured{
+			Object: map[string]interface{}{
+				"apiVersion": fmt.Sprintf("%s/%s", krov1alpha1.KRODomainName, "v1alpha1"),
+				"kind":       "TestDependencyReadiness",
+				"metadata": map[string]interface{}{
+					"name":      instanceName,
+					"namespace": namespace,
+				},
+				"spec": map[string]interface{}{
+					"name": instanceName,
+					"configA": map[string]interface{}{
+						"data":  "valueA",
+						"ready": false,
+					},
+					"configB": map[string]interface{}{
+						"data":  "valueB",
+						"ready": false,
+					},
+					"replicas": 1,
+				},
+			},
+		}
+		Expect(env.Client.Create(ctx, instance)).To(Succeed())
+
+		// Check if instance is created
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Verify ConfigMaps are created
+		configMapA := &corev1.ConfigMap{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName + "-config-a",
+				Namespace: namespace,
+			}, configMapA)
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(configMapA.Data["value"]).To(Equal("valueA"))
+			g.Expect(configMapA.Data["ready"]).To(Equal("false"))
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		configMapB := &corev1.ConfigMap{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName + "-config-b",
+				Namespace: namespace,
+			}, configMapB)
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(configMapB.Data["value"]).To(Equal("valueB"))
+			g.Expect(configMapB.Data["ready"]).To(Equal("false"))
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Verify Deployment is NOT created yet (dependencies not ready)
+		Consistently(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, &appsv1.Deployment{})
+			g.Expect(err).To(MatchError(errors.IsNotFound, "deployment should not be created yet"))
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Verify instance state is IN_PROGRESS
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			status, found, err := unstructured.NestedString(instance.Object, "status", "state")
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(found).To(BeTrue())
+			g.Expect(status).To(Equal("IN_PROGRESS"))
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Update instance spec to set ConfigMap A to ready
+		Eventually(func(g Gomega, ctx SpecContext) {
+			// Get fresh copy to avoid conflicts
+			freshInstance := &unstructured.Unstructured{}
+			freshInstance.SetGroupVersionKind(instance.GroupVersionKind())
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, freshInstance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			err = unstructured.SetNestedField(freshInstance.Object, true, "spec", "configA", "ready")
+			g.Expect(err).ToNot(HaveOccurred())
+
+			err = env.Client.Update(ctx, freshInstance)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 10*time.Second, 500*time.Millisecond).WithContext(ctx).Should(Succeed())
+
+		// Verify deployment is still NOT created (ConfigMap B not ready yet)
+		Consistently(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, &appsv1.Deployment{})
+			g.Expect(err).To(MatchError(errors.IsNotFound, "deployment should still not be created"))
+		}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Update instance spec to set ConfigMap B to ready
+		Eventually(func(g Gomega, ctx SpecContext) {
+			// Get fresh copy to avoid conflicts
+			freshInstance := &unstructured.Unstructured{}
+			freshInstance.SetGroupVersionKind(instance.GroupVersionKind())
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, freshInstance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			err = unstructured.SetNestedField(freshInstance.Object, true, "spec", "configB", "ready")
+			g.Expect(err).ToNot(HaveOccurred())
+
+			err = env.Client.Update(ctx, freshInstance)
+			g.Expect(err).ToNot(HaveOccurred())
+		}, 10*time.Second, 500*time.Millisecond).WithContext(ctx).Should(Succeed())
+
+		// Now verify Deployment IS created (all dependencies are ready)
+		deployment := &appsv1.Deployment{}
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, deployment)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			// Verify deployment specs
+			g.Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
+			g.Expect(*deployment.Spec.Replicas).To(Equal(int32(1)))
+
+			// Verify environment variables from configmaps
+			envVars := deployment.Spec.Template.Spec.Containers[0].Env
+			g.Expect(envVars).To(HaveLen(2))
+
+			var foundConfigA, foundConfigB bool
+			for _, env := range envVars {
+				if env.Name == "CONFIG_A" && env.Value == "valueA" {
+					foundConfigA = true
+				}
+				if env.Name == "CONFIG_B" && env.Value == "valueB" {
+					foundConfigB = true
+				}
+			}
+			g.Expect(foundConfigA).To(BeTrue(), "CONFIG_A should be set from configMapA")
+			g.Expect(foundConfigB).To(BeTrue(), "CONFIG_B should be set from configMapB")
+		}, 30*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Verify instance state becomes ACTIVE once all resources are synced
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).ToNot(HaveOccurred())
+
+			status, found, err := unstructured.NestedString(instance.Object, "status", "state")
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(found).To(BeTrue())
+			g.Expect(status).To(Equal("ACTIVE"))
+		}, 30*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		// Cleanup
+		Expect(env.Client.Delete(ctx, instance)).To(Succeed())
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name:      instanceName,
+				Namespace: namespace,
+			}, instance)
+			g.Expect(err).To(MatchError(errors.IsNotFound, "instance should be deleted"))
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+
+		Expect(env.Client.Delete(ctx, rgd)).To(Succeed())
+		Eventually(func(g Gomega, ctx SpecContext) {
+			err := env.Client.Get(ctx, types.NamespacedName{
+				Name: rgd.Name,
+			}, &krov1alpha1.ResourceGraphDefinition{})
+			g.Expect(err).To(MatchError(errors.IsNotFound, "rgd should be deleted"))
+		}, 20*time.Second, time.Second).WithContext(ctx).Should(Succeed())
+	})
+})