Repository: kubernetes-sigs/kwok
Pull request #1363 (2025-04-29 05:43:17)

Support CEL for Stage
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

/kind feature

#### What this PR does / why we need it:

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
xref #922

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
Support CEL for Stage
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs
NONE
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kwok/pull/1363#" title="Author self-approved">wzshiming</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkwok).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/kwok/blob/main/OWNERS)~~ [wzshiming]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *k8s-kwok* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | ba025943856a81d8fb154260f3ea2c417437024a |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/k8s-kwok/deploys/681066f885b80c00083109ba |
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | ba025943856a81d8fb154260f3ea2c417437024a | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kwok/1363/pull-kwok-verify-main/1917092315982204928) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=1363). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-unit-test-main | 77df5c2825aec7e0a6c3f6f74e5af768da21e803 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kwok/1363/pull-kwok-unit-test-main/1917134125278433280) | true | `/test pull-kwok-unit-test-main`
pull-kwok-verify-main | 77df5c2825aec7e0a6c3f6f74e5af768da21e803 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kwok/1363/pull-kwok-verify-main/1917134125156798464) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=1363). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-unit-test-main | 77df5c2825aec7e0a6c3f6f74e5af768da21e803 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kwok/1363/pull-kwok-unit-test-main/1917134125278433280) | true | `/test pull-kwok-unit-test-main`
pull-kwok-verify-main | 31dafd659301cc2aa1e5920dfce81e6defd16937 | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kwok/1363/pull-kwok-verify-main/1917136949324288000) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=1363). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 34dd12c42650260d49eedfe1b1073098ced72bee | [link](https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_kwok/1363/pull-kwok-verify-main/1917139577584226304) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=1363). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/kustomize/crd/bases/kwok.x-k8s.io_stages.yaml b/kustomize/crd/bases/kwok.x-k8s.io_stages.yaml
index c45698a0..89514a38 100644
--- a/kustomize/crd/bases/kwok.x-k8s.io_stages.yaml
+++ b/kustomize/crd/bases/kwok.x-k8s.io_stages.yaml
@@ -48,10 +48,29 @@ spec:
                       If it is a time.Time type, getting the value will be minus time.Now() to get DurationMilliseconds
                       If it is a string type, the value get will be parsed by time.ParseDuration.
                     properties:
+                      cel:
+                        description: CEL is a Common Expression Language based expression
+                          for value extraction
+                        properties:
+                          expression:
+                            description: Expression represents the expression which
+                              will be evaluated by CEL.
+                            type: string
+                        type: object
                       expressionFrom:
-                        description: ExpressionFrom is the expression used to get
-                          the value.
+                        description: |-
+                          ExpressionFrom is the expression used to get the value.
+                          Deprecated: Use JQ instead.
                         type: string
+                      jq:
+                        description: JQ is a JSON Query based expression for value
+                          extraction
+                        properties:
+                          expression:
+                            description: Expression represents the expression which
+                              will be evaluated by JQ.
+                            type: string
+                        type: object
                     type: object
                   durationMilliseconds:
                     description: |-
@@ -66,10 +85,29 @@ spec:
                       If it is a time.Time type, getting the value will be minus time.Now() to get JitterDurationMilliseconds
                       If it is a string type, the value get will be parsed by time.ParseDuration.
                     properties:
+                      cel:
+                        description: CEL is a Common Expression Language based expression
+                          for value extraction
+                        properties:
+                          expression:
+                            description: Expression represents the expression which
+                              will be evaluated by CEL.
+                            type: string
+                        type: object
                       expressionFrom:
-                        description: ExpressionFrom is the expression used to get
-                          the value.
+                        description: |-
+                          ExpressionFrom is the expression used to get the value.
+                          Deprecated: Use JQ instead.
                         type: string
+                      jq:
+                        description: JQ is a JSON Query based expression for value
+                          extraction
+                        properties:
+                          expression:
+                            description: Expression represents the expression which
+                              will be evaluated by JQ.
+                            type: string
+                        type: object
                     type: object
                   jitterDurationMilliseconds:
                     description: |-
@@ -229,32 +267,60 @@ spec:
                       operator is "In", and the values array contains only "value". The requirements are ANDed.
                     type: object
                   matchExpressions:
-                    description: MatchExpressions is a list of label selector requirements.
+                    description: MatchExpressions is a list of label selector expressions.
                       The requirements are ANDed.
                     items:
-                      description: |-
-                        SelectorRequirement is a resource selector requirement is a selector that contains values, a key,
-                        and an operator that relates the key and values.
+                      description: MatchExpression is a resource selector expression
+                        that must evaluate to true for a resource to be matched.
                       properties:
+                        cel:
+                          description: CEL is a Common Expression Language based selector
+                            expression
+                          properties:
+                            expression:
+                              description: Expression represents the expression which
+                                will be evaluated by CEL.
+                              type: string
+                          type: object
+                        jq:
+                          description: JQ is a JSON Query based selector expression
+                          properties:
+                            key:
+                              description: Key represents the expression which will
+                                be evaluated by JQ.
+                              type: string
+                            operator:
+                              description: Represents a scope's relationship to a
+                                set of values.
+                              type: string
+                            values:
+                              description: |-
+                                An array of string values.
+                                If the operator is In, NotIn, Intersection or NotIntersection, the values array must be non-empty.
+                                If the operator is Exists or DoesNotExist, the values array must be empty.
+                              items:
+                                type: string
+                              type: array
+                          type: object
                         key:
-                          description: The name of the scope that the selector applies
-                            to.
+                          description: |-
+                            Key represents the expression which will be evaluated by JQ.
+                            Deprecated: Use JQ instead.
                           type: string
                         operator:
-                          description: Represents a scope's relationship to a set
-                            of values.
+                          description: |-
+                            Represents a scope's relationship to a set of values.
+                            Deprecated: Use JQ instead.
                           type: string
                         values:
                           description: |-
                             An array of string values.
                             If the operator is In, NotIn, Intersection or NotIntersection, the values array must be non-empty.
                             If the operator is Exists or DoesNotExist, the values array must be empty.
+                            Deprecated: Use JQ instead.
                           items:
                             type: string
                           type: array
-                      required:
-                      - key
-                      - operator
                       type: object
                     type: array
                   matchLabels:
@@ -279,10 +345,28 @@ spec:
                   If it is a number type, convert to int.
                   If it is a string type, the value get will be parsed by strconv.ParseInt.
                 properties:
+                  cel:
+                    description: CEL is a Common Expression Language based expression
+                      for value extraction
+                    properties:
+                      expression:
+                        description: Expression represents the expression which will
+                          be evaluated by CEL.
+                        type: string
+                    type: object
                   expressionFrom:
-                    description: ExpressionFrom is the expression used to get the
-                      value.
+                    description: |-
+                      ExpressionFrom is the expression used to get the value.
+                      Deprecated: Use JQ instead.
                     type: string
+                  jq:
+                    description: JQ is a JSON Query based expression for value extraction
+                    properties:
+                      expression:
+                        description: Expression represents the expression which will
+                          be evaluated by JQ.
+                        type: string
+                    type: object
                 type: object
             required:
             - next
diff --git a/pkg/apis/internalversion/cel_types.go b/pkg/apis/internalversion/cel_types.go
new file mode 100644
index 00000000..40c814b3
--- /dev/null
+++ b/pkg/apis/internalversion/cel_types.go
@@ -0,0 +1,23 @@
+/*
+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 internalversion
+
+// ExpressionCEL is the expression which will be evaluated by CEL.
+type ExpressionCEL struct {
+	// Expression represents the expression which will be evaluated by CEL.
+	Expression string
+}
diff --git a/pkg/apis/internalversion/conversion.go b/pkg/apis/internalversion/conversion.go
index 091e1748..f3c2d9f8 100644
--- a/pkg/apis/internalversion/conversion.go
+++ b/pkg/apis/internalversion/conversion.go
@@ -464,3 +464,26 @@ func Convert_internalversion_StageNext_To_v1alpha1_StageNext(in *StageNext, out
 func Convert_internalversion_StagePatch_To_v1alpha1_StagePatch(in *StagePatch, out *v1alpha1.StagePatch, s conversion.Scope) error {
 	return autoConvert_internalversion_StagePatch_To_v1alpha1_StagePatch(in, out, s)
 }
+
+// Convert_v1alpha1_MatchExpression_To_internalversion_MatchExpression converts a v1alpha1.MatchExpression to an internal version MatchExpression.
+func Convert_v1alpha1_MatchExpression_To_internalversion_MatchExpression(in *v1alpha1.MatchExpression, out *MatchExpression, s conversion.Scope) error {
+	if in.JQ == nil && in.Key != "" {
+		in.JQ = &v1alpha1.SelectorJQ{
+			Key:      in.Key,
+			Operator: in.Operator,
+			Values:   in.Values,
+		}
+	}
+
+	return autoConvert_v1alpha1_MatchExpression_To_internalversion_MatchExpression(in, out, s)
+}
+
+// Convert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom converts a v1alpha1.ExpressionFrom to an internal version ExpressionFrom.
+func Convert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom(in *v1alpha1.ExpressionFrom, out *ExpressionFrom, s conversion.Scope) error {
+	if in.JQ == nil && in.ExpressionFrom != "" {
+		in.JQ = &v1alpha1.ExpressionJQ{
+			Expression: in.ExpressionFrom,
+		}
+	}
+	return autoConvert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom(in, out, s)
+}
diff --git a/pkg/apis/internalversion/jq_types.go b/pkg/apis/internalversion/jq_types.go
new file mode 100644
index 00000000..a3546d94
--- /dev/null
+++ b/pkg/apis/internalversion/jq_types.go
@@ -0,0 +1,51 @@
+/*
+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 internalversion
+
+// ExpressionJQ is the expression which will be evaluated by JQ.
+type ExpressionJQ struct {
+	// Expression represents the expression which will be evaluated by JQ.
+	Expression string
+}
+
+// SelectorJQ is a resource selector requirement is a selector that contains values, a key,
+// and an operator that relates the key and values.
+type SelectorJQ struct {
+	// Key represents the expression which will be evaluated by JQ.
+	Key string
+	// Represents a scope's relationship to a set of values.
+	Operator SelectorOperator
+	// An array of string values.
+	// If the operator is In, NotIn, Intersection or NotIntersection, the values array must be non-empty.
+	// If the operator is Exists or DoesNotExist, the values array must be empty.
+	Values []string
+}
+
+// SelectorOperator is a label selector operator is the set of operators that can be used in a selector requirement.
+type SelectorOperator string
+
+// The following are valid selector operators.
+const (
+	// SelectorOpIn is the set inclusion operator.
+	SelectorOpIn SelectorOperator = "In"
+	// SelectorOpNotIn is the negated set inclusion operator.
+	SelectorOpNotIn SelectorOperator = "NotIn"
+	// SelectorOpExists is the existence operator.
+	SelectorOpExists SelectorOperator = "Exists"
+	// SelectorOpDoesNotExist is the negated existence operator.
+	SelectorOpDoesNotExist SelectorOperator = "DoesNotExist"
+)
diff --git a/pkg/apis/internalversion/stage_types.go b/pkg/apis/internalversion/stage_types.go
index 1fcfd2b5..3b6c3c85 100644
--- a/pkg/apis/internalversion/stage_types.go
+++ b/pkg/apis/internalversion/stage_types.go
@@ -41,7 +41,7 @@ type StageSpec struct {
 	// WeightFrom means is the expression used to get the value.
 	// If it is a number type, convert to int.
 	// If it is a string type, the value get will be parsed by strconv.ParseInt.
-	WeightFrom *ExpressionFromSource
+	WeightFrom *ExpressionFrom
 	// Delay means there is a delay in this stage.
 	Delay *StageDelay
 	// Next indicates that this stage will be moved to.
@@ -66,7 +66,7 @@ type StageDelay struct {
 	// DurationFrom is the expression used to get the value.
 	// If it is a time.Time type, getting the value will be minus time.Now() to get DurationMilliseconds
 	// If it is a string type, the value get will be parsed by time.ParseDuration.
-	DurationFrom *ExpressionFromSource
+	DurationFrom *ExpressionFrom
 
 	// JitterDurationMilliseconds is the duration plus an additional amount chosen uniformly
 	// at random from the interval between DurationMilliseconds and JitterDurationMilliseconds.
@@ -74,7 +74,7 @@ type StageDelay struct {
 	// JitterDurationFrom is the expression used to get the value.
 	// If it is a time.Time type, getting the value will be minus time.Now() to get JitterDurationMilliseconds
 	// If it is a string type, the value get will be parsed by time.ParseDuration.
-	JitterDurationFrom *ExpressionFromSource
+	JitterDurationFrom *ExpressionFrom
 }
 
 // StageNext describes a stage will be moved to.
@@ -162,40 +162,22 @@ type StageSelector struct {
 	// map is equivalent to an element of matchExpressions, whose key field is ".metadata.annotations[key]", the
 	// operator is "In", and the values array contains only "value". The requirements are ANDed.
 	MatchAnnotations map[string]string
-	// MatchExpressions is a list of label selector requirements. The requirements are ANDed.
-	MatchExpressions []SelectorRequirement
+	// MatchExpressions is a list of label selector expressions. The requirements are ANDed.
+	MatchExpressions []MatchExpression
 }
 
-// SelectorRequirement is a resource selector requirement is a selector that contains values, a key,
-// and an operator that relates the key and values.
-type SelectorRequirement struct {
-	// The name of the scope that the selector applies to.
-	Key string
-	// Represents a scope's relationship to a set of values.
-	Operator SelectorOperator
-	// An array of string values.
-	// If the operator is In, NotIn, Intersection or NotIntersection, the values array must be non-empty.
-	// If the operator is Exists or DoesNotExist, the values array must be empty.
-	Values []string
+// MatchExpression is a resource selector expression that must evaluate to true for a resource to be matched.
+type MatchExpression struct {
+	// CEL is a Common Expression Language based selector expression
+	CEL *ExpressionCEL
+	// JQ is a JSON Query based selector expression
+	JQ *SelectorJQ
 }
 
-// SelectorOperator is a label selector operator is the set of operators that can be used in a selector requirement.
-type SelectorOperator string
-
-// The following are valid selector operators.
-const (
-	// SelectorOpIn is the set inclusion operator.
-	SelectorOpIn SelectorOperator = "In"
-	// SelectorOpNotIn is the negated set inclusion operator.
-	SelectorOpNotIn SelectorOperator = "NotIn"
-	// SelectorOpExists is the existence operator.
-	SelectorOpExists SelectorOperator = "Exists"
-	// SelectorOpDoesNotExist is the negated existence operator.
-	SelectorOpDoesNotExist SelectorOperator = "DoesNotExist"
-)
-
-// ExpressionFromSource represents a source for the value of a from.
-type ExpressionFromSource struct {
-	// ExpressionFrom is the expression used to get the value.
-	ExpressionFrom string
+// ExpressionFrom represents a source for extracting values using expressions
+type ExpressionFrom struct {
+	// CEL is a Common Expression Language based expression for value extraction
+	CEL *ExpressionCEL
+	// JQ is a JSON Query based expression for value extraction
+	JQ *ExpressionJQ
 }
diff --git a/pkg/apis/internalversion/zz_generated.conversion.go b/pkg/apis/internalversion/zz_generated.conversion.go
index 778c9d92..55bbbcd9 100644
--- a/pkg/apis/internalversion/zz_generated.conversion.go
+++ b/pkg/apis/internalversion/zz_generated.conversion.go
@@ -260,13 +260,28 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*ExpressionFromSource)(nil), (*v1alpha1.ExpressionFromSource)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_internalversion_ExpressionFromSource_To_v1alpha1_ExpressionFromSource(a.(*ExpressionFromSource), b.(*v1alpha1.ExpressionFromSource), scope)
+	if err := s.AddGeneratedConversionFunc((*ExpressionCEL)(nil), (*v1alpha1.ExpressionCEL)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_internalversion_ExpressionCEL_To_v1alpha1_ExpressionCEL(a.(*ExpressionCEL), b.(*v1alpha1.ExpressionCEL), scope)
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1alpha1.ExpressionFromSource)(nil), (*ExpressionFromSource)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1alpha1_ExpressionFromSource_To_internalversion_ExpressionFromSource(a.(*v1alpha1.ExpressionFromSource), b.(*ExpressionFromSource), scope)
+	if err := s.AddGeneratedConversionFunc((*v1alpha1.ExpressionCEL)(nil), (*ExpressionCEL)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_ExpressionCEL_To_internalversion_ExpressionCEL(a.(*v1alpha1.ExpressionCEL), b.(*ExpressionCEL), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*ExpressionFrom)(nil), (*v1alpha1.ExpressionFrom)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom(a.(*ExpressionFrom), b.(*v1alpha1.ExpressionFrom), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*ExpressionJQ)(nil), (*v1alpha1.ExpressionJQ)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_internalversion_ExpressionJQ_To_v1alpha1_ExpressionJQ(a.(*ExpressionJQ), b.(*v1alpha1.ExpressionJQ), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*v1alpha1.ExpressionJQ)(nil), (*ExpressionJQ)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_ExpressionJQ_To_internalversion_ExpressionJQ(a.(*v1alpha1.ExpressionJQ), b.(*ExpressionJQ), scope)
 	}); err != nil {
 		return err
 	}
@@ -410,6 +425,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*MatchExpression)(nil), (*v1alpha1.MatchExpression)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_internalversion_MatchExpression_To_v1alpha1_MatchExpression(a.(*MatchExpression), b.(*v1alpha1.MatchExpression), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*Metric)(nil), (*v1alpha1.Metric)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_internalversion_Metric_To_v1alpha1_Metric(a.(*Metric), b.(*v1alpha1.Metric), scope)
 	}); err != nil {
@@ -550,13 +570,13 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*SelectorRequirement)(nil), (*v1alpha1.SelectorRequirement)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_internalversion_SelectorRequirement_To_v1alpha1_SelectorRequirement(a.(*SelectorRequirement), b.(*v1alpha1.SelectorRequirement), scope)
+	if err := s.AddGeneratedConversionFunc((*SelectorJQ)(nil), (*v1alpha1.SelectorJQ)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_internalversion_SelectorJQ_To_v1alpha1_SelectorJQ(a.(*SelectorJQ), b.(*v1alpha1.SelectorJQ), scope)
 	}); err != nil {
 		return err
 	}
-	if err := s.AddGeneratedConversionFunc((*v1alpha1.SelectorRequirement)(nil), (*SelectorRequirement)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1alpha1_SelectorRequirement_To_internalversion_SelectorRequirement(a.(*v1alpha1.SelectorRequirement), b.(*SelectorRequirement), scope)
+	if err := s.AddGeneratedConversionFunc((*v1alpha1.SelectorJQ)(nil), (*SelectorJQ)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_SelectorJQ_To_internalversion_SelectorJQ(a.(*v1alpha1.SelectorJQ), b.(*SelectorJQ), scope)
 	}); err != nil {
 		return err
 	}
@@ -665,6 +685,16 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*v1alpha1.ExpressionFrom)(nil), (*ExpressionFrom)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom(a.(*v1alpha1.ExpressionFrom), b.(*ExpressionFrom), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddConversionFunc((*v1alpha1.MatchExpression)(nil), (*MatchExpression)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_MatchExpression_To_internalversion_MatchExpression(a.(*v1alpha1.MatchExpression), b.(*MatchExpression), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddConversionFunc((*v1alpha1.StageNext)(nil), (*StageNext)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha1_StageNext_To_internalversion_StageNext(a.(*v1alpha1.StageNext), b.(*StageNext), scope)
 	}); err != nil {
@@ -1337,24 +1367,62 @@ func Convert_v1alpha1_ExecTargetLocal_To_internalversion_ExecTargetLocal(in *v1a
 	return autoConvert_v1alpha1_ExecTargetLocal_To_internalversion_ExecTargetLocal(in, out, s)
 }
 
-func autoConvert_internalversion_ExpressionFromSource_To_v1alpha1_ExpressionFromSource(in *ExpressionFromSource, out *v1alpha1.ExpressionFromSource, s conversion.Scope) error {
-	out.ExpressionFrom = in.ExpressionFrom
+func autoConvert_internalversion_ExpressionCEL_To_v1alpha1_ExpressionCEL(in *ExpressionCEL, out *v1alpha1.ExpressionCEL, s conversion.Scope) error {
+	out.Expression = in.Expression
 	return nil
 }
 
-// Convert_internalversion_ExpressionFromSource_To_v1alpha1_ExpressionFromSource is an autogenerated conversion function.
-func Convert_internalversion_ExpressionFromSource_To_v1alpha1_ExpressionFromSource(in *ExpressionFromSource, out *v1alpha1.ExpressionFromSource, s conversion.Scope) error {
-	return autoConvert_internalversion_ExpressionFromSource_To_v1alpha1_ExpressionFromSource(in, out, s)
+// Convert_internalversion_ExpressionCEL_To_v1alpha1_ExpressionCEL is an autogenerated conversion function.
+func Convert_internalversion_ExpressionCEL_To_v1alpha1_ExpressionCEL(in *ExpressionCEL, out *v1alpha1.ExpressionCEL, s conversion.Scope) error {
+	return autoConvert_internalversion_ExpressionCEL_To_v1alpha1_ExpressionCEL(in, out, s)
 }
 
-func autoConvert_v1alpha1_ExpressionFromSource_To_internalversion_ExpressionFromSource(in *v1alpha1.ExpressionFromSource, out *ExpressionFromSource, s conversion.Scope) error {
-	out.ExpressionFrom = in.ExpressionFrom
+func autoConvert_v1alpha1_ExpressionCEL_To_internalversion_ExpressionCEL(in *v1alpha1.ExpressionCEL, out *ExpressionCEL, s conversion.Scope) error {
+	out.Expression = in.Expression
 	return nil
 }
 
-// Convert_v1alpha1_ExpressionFromSource_To_internalversion_ExpressionFromSource is an autogenerated conversion function.
-func Convert_v1alpha1_ExpressionFromSource_To_internalversion_ExpressionFromSource(in *v1alpha1.ExpressionFromSource, out *ExpressionFromSource, s conversion.Scope) error {
-	return autoConvert_v1alpha1_ExpressionFromSource_To_internalversion_ExpressionFromSource(in, out, s)
+// Convert_v1alpha1_ExpressionCEL_To_internalversion_ExpressionCEL is an autogenerated conversion function.
+func Convert_v1alpha1_ExpressionCEL_To_internalversion_ExpressionCEL(in *v1alpha1.ExpressionCEL, out *ExpressionCEL, s conversion.Scope) error {
+	return autoConvert_v1alpha1_ExpressionCEL_To_internalversion_ExpressionCEL(in, out, s)
+}
+
+func autoConvert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom(in *ExpressionFrom, out *v1alpha1.ExpressionFrom, s conversion.Scope) error {
+	out.CEL = (*v1alpha1.ExpressionCEL)(unsafe.Pointer(in.CEL))
+	out.JQ = (*v1alpha1.ExpressionJQ)(unsafe.Pointer(in.JQ))
+	return nil
+}
+
+// Convert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom is an autogenerated conversion function.
+func Convert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom(in *ExpressionFrom, out *v1alpha1.ExpressionFrom, s conversion.Scope) error {
+	return autoConvert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom(in, out, s)
+}
+
+func autoConvert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom(in *v1alpha1.ExpressionFrom, out *ExpressionFrom, s conversion.Scope) error {
+	out.CEL = (*ExpressionCEL)(unsafe.Pointer(in.CEL))
+	out.JQ = (*ExpressionJQ)(unsafe.Pointer(in.JQ))
+	// INFO: in.ExpressionFrom opted out of conversion generation
+	return nil
+}
+
+func autoConvert_internalversion_ExpressionJQ_To_v1alpha1_ExpressionJQ(in *ExpressionJQ, out *v1alpha1.ExpressionJQ, s conversion.Scope) error {
+	out.Expression = in.Expression
+	return nil
+}
+
+// Convert_internalversion_ExpressionJQ_To_v1alpha1_ExpressionJQ is an autogenerated conversion function.
+func Convert_internalversion_ExpressionJQ_To_v1alpha1_ExpressionJQ(in *ExpressionJQ, out *v1alpha1.ExpressionJQ, s conversion.Scope) error {
+	return autoConvert_internalversion_ExpressionJQ_To_v1alpha1_ExpressionJQ(in, out, s)
+}
+
+func autoConvert_v1alpha1_ExpressionJQ_To_internalversion_ExpressionJQ(in *v1alpha1.ExpressionJQ, out *ExpressionJQ, s conversion.Scope) error {
+	out.Expression = in.Expression
+	return nil
+}
+
+// Convert_v1alpha1_ExpressionJQ_To_internalversion_ExpressionJQ is an autogenerated conversion function.
+func Convert_v1alpha1_ExpressionJQ_To_internalversion_ExpressionJQ(in *v1alpha1.ExpressionJQ, out *ExpressionJQ, s conversion.Scope) error {
+	return autoConvert_v1alpha1_ExpressionJQ_To_internalversion_ExpressionJQ(in, out, s)
 }
 
 func autoConvert_internalversion_ExtraArgs_To_v1alpha1_ExtraArgs(in *ExtraArgs, out *configv1alpha1.ExtraArgs, s conversion.Scope) error {
@@ -2048,6 +2116,26 @@ func Convert_v1alpha1_LogsSpec_To_internalversion_LogsSpec(in *v1alpha1.LogsSpec
 	return autoConvert_v1alpha1_LogsSpec_To_internalversion_LogsSpec(in, out, s)
 }
 
+func autoConvert_internalversion_MatchExpression_To_v1alpha1_MatchExpression(in *MatchExpression, out *v1alpha1.MatchExpression, s conversion.Scope) error {
+	out.CEL = (*v1alpha1.ExpressionCEL)(unsafe.Pointer(in.CEL))
+	out.JQ = (*v1alpha1.SelectorJQ)(unsafe.Pointer(in.JQ))
+	return nil
+}
+
+// Convert_internalversion_MatchExpression_To_v1alpha1_MatchExpression is an autogenerated conversion function.
+func Convert_internalversion_MatchExpression_To_v1alpha1_MatchExpression(in *MatchExpression, out *v1alpha1.MatchExpression, s conversion.Scope) error {
+	return autoConvert_internalversion_MatchExpression_To_v1alpha1_MatchExpression(in, out, s)
+}
+
+func autoConvert_v1alpha1_MatchExpression_To_internalversion_MatchExpression(in *v1alpha1.MatchExpression, out *MatchExpression, s conversion.Scope) error {
+	out.CEL = (*ExpressionCEL)(unsafe.Pointer(in.CEL))
+	out.JQ = (*SelectorJQ)(unsafe.Pointer(in.JQ))
+	// INFO: in.Key opted out of conversion generation
+	// INFO: in.Operator opted out of conversion generation
+	// INFO: in.Values opted out of conversion generation
+	return nil
+}
+
 func autoConvert_internalversion_Metric_To_v1alpha1_Metric(in *Metric, out *v1alpha1.Metric, s conversion.Scope) error {
 	out.ObjectMeta = in.ObjectMeta
 	if err := Convert_internalversion_MetricSpec_To_v1alpha1_MetricSpec(&in.Spec, &out.Spec, s); err != nil {
@@ -2386,28 +2474,28 @@ func Convert_v1alpha1_SecurityContext_To_internalversion_SecurityContext(in *v1a
 	return autoConvert_v1alpha1_SecurityContext_To_internalversion_SecurityContext(in, out, s)
 }
 
-func autoConvert_internalversion_SelectorRequirement_To_v1alpha1_SelectorRequirement(in *SelectorRequirement, out *v1alpha1.SelectorRequirement, s conversion.Scope) error {
+func autoConvert_internalversion_SelectorJQ_To_v1alpha1_SelectorJQ(in *SelectorJQ, out *v1alpha1.SelectorJQ, s conversion.Scope) error {
 	out.Key = in.Key
 	out.Operator = v1alpha1.SelectorOperator(in.Operator)
 	out.Values = *(*[]string)(unsafe.Pointer(&in.Values))
 	return nil
 }
 
-// Convert_internalversion_SelectorRequirement_To_v1alpha1_SelectorRequirement is an autogenerated conversion function.
-func Convert_internalversion_SelectorRequirement_To_v1alpha1_SelectorRequirement(in *SelectorRequirement, out *v1alpha1.SelectorRequirement, s conversion.Scope) error {
-	return autoConvert_internalversion_SelectorRequirement_To_v1alpha1_SelectorRequirement(in, out, s)
+// Convert_internalversion_SelectorJQ_To_v1alpha1_SelectorJQ is an autogenerated conversion function.
+func Convert_internalversion_SelectorJQ_To_v1alpha1_SelectorJQ(in *SelectorJQ, out *v1alpha1.SelectorJQ, s conversion.Scope) error {
+	return autoConvert_internalversion_SelectorJQ_To_v1alpha1_SelectorJQ(in, out, s)
 }
 
-func autoConvert_v1alpha1_SelectorRequirement_To_internalversion_SelectorRequirement(in *v1alpha1.SelectorRequirement, out *SelectorRequirement, s conversion.Scope) error {
+func autoConvert_v1alpha1_SelectorJQ_To_internalversion_SelectorJQ(in *v1alpha1.SelectorJQ, out *SelectorJQ, s conversion.Scope) error {
 	out.Key = in.Key
 	out.Operator = SelectorOperator(in.Operator)
 	out.Values = *(*[]string)(unsafe.Pointer(&in.Values))
 	return nil
 }
 
-// Convert_v1alpha1_SelectorRequirement_To_internalversion_SelectorRequirement is an autogenerated conversion function.
-func Convert_v1alpha1_SelectorRequirement_To_internalversion_SelectorRequirement(in *v1alpha1.SelectorRequirement, out *SelectorRequirement, s conversion.Scope) error {
-	return autoConvert_v1alpha1_SelectorRequirement_To_internalversion_SelectorRequirement(in, out, s)
+// Convert_v1alpha1_SelectorJQ_To_internalversion_SelectorJQ is an autogenerated conversion function.
+func Convert_v1alpha1_SelectorJQ_To_internalversion_SelectorJQ(in *v1alpha1.SelectorJQ, out *SelectorJQ, s conversion.Scope) error {
+	return autoConvert_v1alpha1_SelectorJQ_To_internalversion_SelectorJQ(in, out, s)
 }
 
 func autoConvert_internalversion_Stage_To_v1alpha1_Stage(in *Stage, out *v1alpha1.Stage, s conversion.Scope) error {
@@ -2440,9 +2528,25 @@ func Convert_v1alpha1_Stage_To_internalversion_Stage(in *v1alpha1.Stage, out *St
 
 func autoConvert_internalversion_StageDelay_To_v1alpha1_StageDelay(in *StageDelay, out *v1alpha1.StageDelay, s conversion.Scope) error {
 	out.DurationMilliseconds = (*int64)(unsafe.Pointer(in.DurationMilliseconds))
-	out.DurationFrom = (*v1alpha1.ExpressionFromSource)(unsafe.Pointer(in.DurationFrom))
+	if in.DurationFrom != nil {
+		in, out := &in.DurationFrom, &out.DurationFrom
+		*out = new(v1alpha1.ExpressionFrom)
+		if err := Convert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.DurationFrom = nil
+	}
 	out.JitterDurationMilliseconds = (*int64)(unsafe.Pointer(in.JitterDurationMilliseconds))
-	out.JitterDurationFrom = (*v1alpha1.ExpressionFromSource)(unsafe.Pointer(in.JitterDurationFrom))
+	if in.JitterDurationFrom != nil {
+		in, out := &in.JitterDurationFrom, &out.JitterDurationFrom
+		*out = new(v1alpha1.ExpressionFrom)
+		if err := Convert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.JitterDurationFrom = nil
+	}
 	return nil
 }
 
@@ -2453,9 +2557,25 @@ func Convert_internalversion_StageDelay_To_v1alpha1_StageDelay(in *StageDelay, o
 
 func autoConvert_v1alpha1_StageDelay_To_internalversion_StageDelay(in *v1alpha1.StageDelay, out *StageDelay, s conversion.Scope) error {
 	out.DurationMilliseconds = (*int64)(unsafe.Pointer(in.DurationMilliseconds))
-	out.DurationFrom = (*ExpressionFromSource)(unsafe.Pointer(in.DurationFrom))
+	if in.DurationFrom != nil {
+		in, out := &in.DurationFrom, &out.DurationFrom
+		*out = new(ExpressionFrom)
+		if err := Convert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.DurationFrom = nil
+	}
 	out.JitterDurationMilliseconds = (*int64)(unsafe.Pointer(in.JitterDurationMilliseconds))
-	out.JitterDurationFrom = (*ExpressionFromSource)(unsafe.Pointer(in.JitterDurationFrom))
+	if in.JitterDurationFrom != nil {
+		in, out := &in.JitterDurationFrom, &out.JitterDurationFrom
+		*out = new(ExpressionFrom)
+		if err := Convert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.JitterDurationFrom = nil
+	}
 	return nil
 }
 
@@ -2599,7 +2719,17 @@ func Convert_v1alpha1_StageResourceRef_To_internalversion_StageResourceRef(in *v
 func autoConvert_internalversion_StageSelector_To_v1alpha1_StageSelector(in *StageSelector, out *v1alpha1.StageSelector, s conversion.Scope) error {
 	out.MatchLabels = *(*map[string]string)(unsafe.Pointer(&in.MatchLabels))
 	out.MatchAnnotations = *(*map[string]string)(unsafe.Pointer(&in.MatchAnnotations))
-	out.MatchExpressions = *(*[]v1alpha1.SelectorRequirement)(unsafe.Pointer(&in.MatchExpressions))
+	if in.MatchExpressions != nil {
+		in, out := &in.MatchExpressions, &out.MatchExpressions
+		*out = make([]v1alpha1.MatchExpression, len(*in))
+		for i := range *in {
+			if err := Convert_internalversion_MatchExpression_To_v1alpha1_MatchExpression(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.MatchExpressions = nil
+	}
 	return nil
 }
 
@@ -2611,7 +2741,17 @@ func Convert_internalversion_StageSelector_To_v1alpha1_StageSelector(in *StageSe
 func autoConvert_v1alpha1_StageSelector_To_internalversion_StageSelector(in *v1alpha1.StageSelector, out *StageSelector, s conversion.Scope) error {
 	out.MatchLabels = *(*map[string]string)(unsafe.Pointer(&in.MatchLabels))
 	out.MatchAnnotations = *(*map[string]string)(unsafe.Pointer(&in.MatchAnnotations))
-	out.MatchExpressions = *(*[]SelectorRequirement)(unsafe.Pointer(&in.MatchExpressions))
+	if in.MatchExpressions != nil {
+		in, out := &in.MatchExpressions, &out.MatchExpressions
+		*out = make([]MatchExpression, len(*in))
+		for i := range *in {
+			if err := Convert_v1alpha1_MatchExpression_To_internalversion_MatchExpression(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.MatchExpressions = nil
+	}
 	return nil
 }
 
@@ -2624,10 +2764,34 @@ func autoConvert_internalversion_StageSpec_To_v1alpha1_StageSpec(in *StageSpec,
 	if err := Convert_internalversion_StageResourceRef_To_v1alpha1_StageResourceRef(&in.ResourceRef, &out.ResourceRef, s); err != nil {
 		return err
 	}
-	out.Selector = (*v1alpha1.StageSelector)(unsafe.Pointer(in.Selector))
+	if in.Selector != nil {
+		in, out := &in.Selector, &out.Selector
+		*out = new(v1alpha1.StageSelector)
+		if err := Convert_internalversion_StageSelector_To_v1alpha1_StageSelector(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.Selector = nil
+	}
 	out.Weight = in.Weight
-	out.WeightFrom = (*v1alpha1.ExpressionFromSource)(unsafe.Pointer(in.WeightFrom))
-	out.Delay = (*v1alpha1.StageDelay)(unsafe.Pointer(in.Delay))
+	if in.WeightFrom != nil {
+		in, out := &in.WeightFrom, &out.WeightFrom
+		*out = new(v1alpha1.ExpressionFrom)
+		if err := Convert_internalversion_ExpressionFrom_To_v1alpha1_ExpressionFrom(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.WeightFrom = nil
+	}
+	if in.Delay != nil {
+		in, out := &in.Delay, &out.Delay
+		*out = new(v1alpha1.StageDelay)
+		if err := Convert_internalversion_StageDelay_To_v1alpha1_StageDelay(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.Delay = nil
+	}
 	if err := Convert_internalversion_StageNext_To_v1alpha1_StageNext(&in.Next, &out.Next, s); err != nil {
 		return err
 	}
@@ -2646,10 +2810,34 @@ func autoConvert_v1alpha1_StageSpec_To_internalversion_StageSpec(in *v1alpha1.St
 	if err := Convert_v1alpha1_StageResourceRef_To_internalversion_StageResourceRef(&in.ResourceRef, &out.ResourceRef, s); err != nil {
 		return err
 	}
-	out.Selector = (*StageSelector)(unsafe.Pointer(in.Selector))
+	if in.Selector != nil {
+		in, out := &in.Selector, &out.Selector
+		*out = new(StageSelector)
+		if err := Convert_v1alpha1_StageSelector_To_internalversion_StageSelector(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.Selector = nil
+	}
 	out.Weight = in.Weight
-	out.WeightFrom = (*ExpressionFromSource)(unsafe.Pointer(in.WeightFrom))
-	out.Delay = (*StageDelay)(unsafe.Pointer(in.Delay))
+	if in.WeightFrom != nil {
+		in, out := &in.WeightFrom, &out.WeightFrom
+		*out = new(ExpressionFrom)
+		if err := Convert_v1alpha1_ExpressionFrom_To_internalversion_ExpressionFrom(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.WeightFrom = nil
+	}
+	if in.Delay != nil {
+		in, out := &in.Delay, &out.Delay
+		*out = new(StageDelay)
+		if err := Convert_v1alpha1_StageDelay_To_internalversion_StageDelay(*in, *out, s); err != nil {
+			return err
+		}
+	} else {
+		out.Delay = nil
+	}
 	if err := Convert_v1alpha1_StageNext_To_internalversion_StageNext(&in.Next, &out.Next, s); err != nil {
 		return err
 	}
diff --git a/pkg/apis/internalversion/zz_generated.deepcopy.go b/pkg/apis/internalversion/zz_generated.deepcopy.go
index 0dcb7848..013e235a 100644
--- a/pkg/apis/internalversion/zz_generated.deepcopy.go
+++ b/pkg/apis/internalversion/zz_generated.deepcopy.go
@@ -546,17 +546,59 @@ func (in *ExecTargetLocal) DeepCopy() *ExecTargetLocal {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ExpressionFromSource) DeepCopyInto(out *ExpressionFromSource) {
+func (in *ExpressionCEL) DeepCopyInto(out *ExpressionCEL) {
 	*out = *in
 	return
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionFromSource.
-func (in *ExpressionFromSource) DeepCopy() *ExpressionFromSource {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionCEL.
+func (in *ExpressionCEL) DeepCopy() *ExpressionCEL {
 	if in == nil {
 		return nil
 	}
-	out := new(ExpressionFromSource)
+	out := new(ExpressionCEL)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExpressionFrom) DeepCopyInto(out *ExpressionFrom) {
+	*out = *in
+	if in.CEL != nil {
+		in, out := &in.CEL, &out.CEL
+		*out = new(ExpressionCEL)
+		**out = **in
+	}
+	if in.JQ != nil {
+		in, out := &in.JQ, &out.JQ
+		*out = new(ExpressionJQ)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionFrom.
+func (in *ExpressionFrom) DeepCopy() *ExpressionFrom {
+	if in == nil {
+		return nil
+	}
+	out := new(ExpressionFrom)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExpressionJQ) DeepCopyInto(out *ExpressionJQ) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionJQ.
+func (in *ExpressionJQ) DeepCopy() *ExpressionJQ {
+	if in == nil {
+		return nil
+	}
+	out := new(ExpressionJQ)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -875,6 +917,32 @@ func (in *LogsSpec) DeepCopy() *LogsSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MatchExpression) DeepCopyInto(out *MatchExpression) {
+	*out = *in
+	if in.CEL != nil {
+		in, out := &in.CEL, &out.CEL
+		*out = new(ExpressionCEL)
+		**out = **in
+	}
+	if in.JQ != nil {
+		in, out := &in.JQ, &out.JQ
+		*out = new(SelectorJQ)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchExpression.
+func (in *MatchExpression) DeepCopy() *MatchExpression {
+	if in == nil {
+		return nil
+	}
+	out := new(MatchExpression)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Metric) DeepCopyInto(out *Metric) {
 	*out = *in
@@ -1179,7 +1247,7 @@ func (in *SecurityContext) DeepCopy() *SecurityContext {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *SelectorRequirement) DeepCopyInto(out *SelectorRequirement) {
+func (in *SelectorJQ) DeepCopyInto(out *SelectorJQ) {
 	*out = *in
 	if in.Values != nil {
 		in, out := &in.Values, &out.Values
@@ -1189,12 +1257,12 @@ func (in *SelectorRequirement) DeepCopyInto(out *SelectorRequirement) {
 	return
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelectorRequirement.
-func (in *SelectorRequirement) DeepCopy() *SelectorRequirement {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelectorJQ.
+func (in *SelectorJQ) DeepCopy() *SelectorJQ {
 	if in == nil {
 		return nil
 	}
-	out := new(SelectorRequirement)
+	out := new(SelectorJQ)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -1227,8 +1295,8 @@ func (in *StageDelay) DeepCopyInto(out *StageDelay) {
 	}
 	if in.DurationFrom != nil {
 		in, out := &in.DurationFrom, &out.DurationFrom
-		*out = new(ExpressionFromSource)
-		**out = **in
+		*out = new(ExpressionFrom)
+		(*in).DeepCopyInto(*out)
 	}
 	if in.JitterDurationMilliseconds != nil {
 		in, out := &in.JitterDurationMilliseconds, &out.JitterDurationMilliseconds
@@ -1237,8 +1305,8 @@ func (in *StageDelay) DeepCopyInto(out *StageDelay) {
 	}
 	if in.JitterDurationFrom != nil {
 		in, out := &in.JitterDurationFrom, &out.JitterDurationFrom
-		*out = new(ExpressionFromSource)
-		**out = **in
+		*out = new(ExpressionFrom)
+		(*in).DeepCopyInto(*out)
 	}
 	return
 }
@@ -1389,7 +1457,7 @@ func (in *StageSelector) DeepCopyInto(out *StageSelector) {
 	}
 	if in.MatchExpressions != nil {
 		in, out := &in.MatchExpressions, &out.MatchExpressions
-		*out = make([]SelectorRequirement, len(*in))
+		*out = make([]MatchExpression, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -1418,8 +1486,8 @@ func (in *StageSpec) DeepCopyInto(out *StageSpec) {
 	}
 	if in.WeightFrom != nil {
 		in, out := &in.WeightFrom, &out.WeightFrom
-		*out = new(ExpressionFromSource)
-		**out = **in
+		*out = new(ExpressionFrom)
+		(*in).DeepCopyInto(*out)
 	}
 	if in.Delay != nil {
 		in, out := &in.Delay, &out.Delay
diff --git a/pkg/apis/v1alpha1/cel_types.go b/pkg/apis/v1alpha1/cel_types.go
new file mode 100644
index 00000000..07353f8a
--- /dev/null
+++ b/pkg/apis/v1alpha1/cel_types.go
@@ -0,0 +1,23 @@
+/*
+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 v1alpha1
+
+// ExpressionCEL is the expression which will be evaluated by CEL.
+type ExpressionCEL struct {
+	// Expression represents the expression which will be evaluated by CEL.
+	Expression string `json:"expression,omitempty"`
+}
diff --git a/pkg/apis/v1alpha1/jq_types.go b/pkg/apis/v1alpha1/jq_types.go
new file mode 100644
index 00000000..52c91da3
--- /dev/null
+++ b/pkg/apis/v1alpha1/jq_types.go
@@ -0,0 +1,52 @@
+/*
+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 v1alpha1
+
+// ExpressionJQ is the expression which will be evaluated by JQ.
+type ExpressionJQ struct {
+	// Expression represents the expression which will be evaluated by JQ.
+	Expression string `json:"expression,omitempty"`
+}
+
+// SelectorJQ is a resource selector requirement is a selector that contains values, a key,
+// and an operator that relates the key and values.
+type SelectorJQ struct {
+	// Key represents the expression which will be evaluated by JQ.
+	Key string `json:"key,omitempty"`
+	// Represents a scope's relationship to a set of values.
+	Operator SelectorOperator `json:"operator,omitempty"`
+	// An array of string values.
+	// If the operator is In, NotIn, Intersection or NotIntersection, the values array must be non-empty.
+	// If the operator is Exists or DoesNotExist, the values array must be empty.
+	Values []string `json:"values,omitempty"`
+}
+
+// SelectorOperator is a label selector operator is the set of operators that can be used in a selector requirement.
+// +enum
+type SelectorOperator string
+
+// The following are valid selector operators.
+const (
+	// SelectorOpIn is the set inclusion operator.
+	SelectorOpIn SelectorOperator = "In"
+	// SelectorOpNotIn is the negated set inclusion operator.
+	SelectorOpNotIn SelectorOperator = "NotIn"
+	// SelectorOpExists is the existence operator.
+	SelectorOpExists SelectorOperator = "Exists"
+	// SelectorOpDoesNotExist is the negated existence operator.
+	SelectorOpDoesNotExist SelectorOperator = "DoesNotExist"
+)
diff --git a/pkg/apis/v1alpha1/stage_types.go b/pkg/apis/v1alpha1/stage_types.go
index a26d0490..2a5cfcf7 100644
--- a/pkg/apis/v1alpha1/stage_types.go
+++ b/pkg/apis/v1alpha1/stage_types.go
@@ -72,7 +72,7 @@ type StageSpec struct {
 	// WeightFrom means is the expression used to get the value.
 	// If it is a number type, convert to int.
 	// If it is a string type, the value get will be parsed by strconv.ParseInt.
-	WeightFrom *ExpressionFromSource `json:"weightFrom,omitempty"`
+	WeightFrom *ExpressionFrom `json:"weightFrom,omitempty"`
 	// Delay means there is a delay in this stage.
 	Delay *StageDelay `json:"delay,omitempty"`
 	// Next indicates that this stage will be moved to.
@@ -100,7 +100,7 @@ type StageDelay struct {
 	// DurationFrom is the expression used to get the value.
 	// If it is a time.Time type, getting the value will be minus time.Now() to get DurationMilliseconds
 	// If it is a string type, the value get will be parsed by time.ParseDuration.
-	DurationFrom *ExpressionFromSource `json:"durationFrom,omitempty"`
+	DurationFrom *ExpressionFrom `json:"durationFrom,omitempty"`
 
 	// JitterDurationMilliseconds is the duration plus an additional amount chosen uniformly
 	// at random from the interval between DurationMilliseconds and JitterDurationMilliseconds.
@@ -109,7 +109,7 @@ type StageDelay struct {
 	// JitterDurationFrom is the expression used to get the value.
 	// If it is a time.Time type, getting the value will be minus time.Now() to get JitterDurationMilliseconds
 	// If it is a string type, the value get will be parsed by time.ParseDuration.
-	JitterDurationFrom *ExpressionFromSource `json:"jitterDurationFrom,omitempty"`
+	JitterDurationFrom *ExpressionFrom `json:"jitterDurationFrom,omitempty"`
 }
 
 // StageNext describes a stage will be moved to.
@@ -217,42 +217,43 @@ type StageSelector struct {
 	// map is equivalent to an element of matchExpressions, whose key field is ".metadata.annotations[key]", the
 	// operator is "In", and the values array contains only "value". The requirements are ANDed.
 	MatchAnnotations map[string]string `json:"matchAnnotations,omitempty"`
-	// MatchExpressions is a list of label selector requirements. The requirements are ANDed.
-	MatchExpressions []SelectorRequirement `json:"matchExpressions,omitempty"`
+	// MatchExpressions is a list of label selector expressions. The requirements are ANDed.
+	MatchExpressions []MatchExpression `json:"matchExpressions,omitempty"`
 }
 
-// SelectorRequirement is a resource selector requirement is a selector that contains values, a key,
-// and an operator that relates the key and values.
-type SelectorRequirement struct {
-	// The name of the scope that the selector applies to.
-	Key string `json:"key"`
+// MatchExpression is a resource selector expression that must evaluate to true for a resource to be matched.
+type MatchExpression struct {
+	// CEL is a Common Expression Language based selector expression
+	CEL *ExpressionCEL `json:"cel,omitempty"`
+	// JQ is a JSON Query based selector expression
+	JQ *SelectorJQ `json:"jq,omitempty"`
+
+	// Key represents the expression which will be evaluated by JQ.
+	// Deprecated: Use JQ instead.
+	//+k8s:conversion-gen=false
+	Key string `json:"key,omitempty"`
 	// Represents a scope's relationship to a set of values.
-	Operator SelectorOperator `json:"operator"`
+	// Deprecated: Use JQ instead.
+	//+k8s:conversion-gen=false
+	Operator SelectorOperator `json:"operator,omitempty"`
 	// An array of string values.
 	// If the operator is In, NotIn, Intersection or NotIntersection, the values array must be non-empty.
 	// If the operator is Exists or DoesNotExist, the values array must be empty.
+	// Deprecated: Use JQ instead.
+	//+k8s:conversion-gen=false
 	Values []string `json:"values,omitempty"`
 }
 
-// SelectorOperator is a label selector operator is the set of operators that can be used in a selector requirement.
-// +enum
-type SelectorOperator string
+// ExpressionFrom represents a source for extracting values using expressions
+type ExpressionFrom struct {
+	// CEL is a Common Expression Language based expression for value extraction
+	CEL *ExpressionCEL `json:"cel,omitempty"`
+	// JQ is a JSON Query based expression for value extraction
+	JQ *ExpressionJQ `json:"jq,omitempty"`
 
-// The following are valid selector operators.
-const (
-	// SelectorOpIn is the set inclusion operator.
-	SelectorOpIn SelectorOperator = "In"
-	// SelectorOpNotIn is the negated set inclusion operator.
-	SelectorOpNotIn SelectorOperator = "NotIn"
-	// SelectorOpExists is the existence operator.
-	SelectorOpExists SelectorOperator = "Exists"
-	// SelectorOpDoesNotExist is the negated existence operator.
-	SelectorOpDoesNotExist SelectorOperator = "DoesNotExist"
-)
-
-// ExpressionFromSource represents a source for the value of a from.
-type ExpressionFromSource struct {
 	// ExpressionFrom is the expression used to get the value.
+	// Deprecated: Use JQ instead.
+	//+k8s:conversion-gen=false
 	ExpressionFrom string `json:"expressionFrom,omitempty"`
 }
 
diff --git a/pkg/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/v1alpha1/zz_generated.deepcopy.go
index 3f445815..0d178010 100644
--- a/pkg/apis/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/v1alpha1/zz_generated.deepcopy.go
@@ -911,17 +911,59 @@ func (in *ExecTargetLocal) DeepCopy() *ExecTargetLocal {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ExpressionFromSource) DeepCopyInto(out *ExpressionFromSource) {
+func (in *ExpressionCEL) DeepCopyInto(out *ExpressionCEL) {
 	*out = *in
 	return
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionFromSource.
-func (in *ExpressionFromSource) DeepCopy() *ExpressionFromSource {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionCEL.
+func (in *ExpressionCEL) DeepCopy() *ExpressionCEL {
 	if in == nil {
 		return nil
 	}
-	out := new(ExpressionFromSource)
+	out := new(ExpressionCEL)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExpressionFrom) DeepCopyInto(out *ExpressionFrom) {
+	*out = *in
+	if in.CEL != nil {
+		in, out := &in.CEL, &out.CEL
+		*out = new(ExpressionCEL)
+		**out = **in
+	}
+	if in.JQ != nil {
+		in, out := &in.JQ, &out.JQ
+		*out = new(ExpressionJQ)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionFrom.
+func (in *ExpressionFrom) DeepCopy() *ExpressionFrom {
+	if in == nil {
+		return nil
+	}
+	out := new(ExpressionFrom)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExpressionJQ) DeepCopyInto(out *ExpressionJQ) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionJQ.
+func (in *ExpressionJQ) DeepCopy() *ExpressionJQ {
+	if in == nil {
+		return nil
+	}
+	out := new(ExpressionJQ)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -1148,6 +1190,37 @@ func (in *LogsStatus) DeepCopy() *LogsStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MatchExpression) DeepCopyInto(out *MatchExpression) {
+	*out = *in
+	if in.CEL != nil {
+		in, out := &in.CEL, &out.CEL
+		*out = new(ExpressionCEL)
+		**out = **in
+	}
+	if in.JQ != nil {
+		in, out := &in.JQ, &out.JQ
+		*out = new(SelectorJQ)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Values != nil {
+		in, out := &in.Values, &out.Values
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchExpression.
+func (in *MatchExpression) DeepCopy() *MatchExpression {
+	if in == nil {
+		return nil
+	}
+	out := new(MatchExpression)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Metric) DeepCopyInto(out *Metric) {
 	*out = *in
@@ -1634,7 +1707,7 @@ func (in *SecurityContext) DeepCopy() *SecurityContext {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *SelectorRequirement) DeepCopyInto(out *SelectorRequirement) {
+func (in *SelectorJQ) DeepCopyInto(out *SelectorJQ) {
 	*out = *in
 	if in.Values != nil {
 		in, out := &in.Values, &out.Values
@@ -1644,12 +1717,12 @@ func (in *SelectorRequirement) DeepCopyInto(out *SelectorRequirement) {
 	return
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelectorRequirement.
-func (in *SelectorRequirement) DeepCopy() *SelectorRequirement {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelectorJQ.
+func (in *SelectorJQ) DeepCopy() *SelectorJQ {
 	if in == nil {
 		return nil
 	}
-	out := new(SelectorRequirement)
+	out := new(SelectorJQ)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -1692,8 +1765,8 @@ func (in *StageDelay) DeepCopyInto(out *StageDelay) {
 	}
 	if in.DurationFrom != nil {
 		in, out := &in.DurationFrom, &out.DurationFrom
-		*out = new(ExpressionFromSource)
-		**out = **in
+		*out = new(ExpressionFrom)
+		(*in).DeepCopyInto(*out)
 	}
 	if in.JitterDurationMilliseconds != nil {
 		in, out := &in.JitterDurationMilliseconds, &out.JitterDurationMilliseconds
@@ -1702,8 +1775,8 @@ func (in *StageDelay) DeepCopyInto(out *StageDelay) {
 	}
 	if in.JitterDurationFrom != nil {
 		in, out := &in.JitterDurationFrom, &out.JitterDurationFrom
-		*out = new(ExpressionFromSource)
-		**out = **in
+		*out = new(ExpressionFrom)
+		(*in).DeepCopyInto(*out)
 	}
 	return
 }
@@ -1897,7 +1970,7 @@ func (in *StageSelector) DeepCopyInto(out *StageSelector) {
 	}
 	if in.MatchExpressions != nil {
 		in, out := &in.MatchExpressions, &out.MatchExpressions
-		*out = make([]SelectorRequirement, len(*in))
+		*out = make([]MatchExpression, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -1926,8 +1999,8 @@ func (in *StageSpec) DeepCopyInto(out *StageSpec) {
 	}
 	if in.WeightFrom != nil {
 		in, out := &in.WeightFrom, &out.WeightFrom
-		*out = new(ExpressionFromSource)
-		**out = **in
+		*out = new(ExpressionFrom)
+		(*in).DeepCopyInto(*out)
 	}
 	if in.Delay != nil {
 		in, out := &in.Delay, &out.Delay
diff --git a/pkg/kwok/controllers/node_controller.go b/pkg/kwok/controllers/node_controller.go
index ce050621..d03f2382 100644
--- a/pkg/kwok/controllers/node_controller.go
+++ b/pkg/kwok/controllers/node_controller.go
@@ -33,7 +33,6 @@ import (
 
 	"sigs.k8s.io/kwok/pkg/config/resources"
 	"sigs.k8s.io/kwok/pkg/log"
-	"sigs.k8s.io/kwok/pkg/utils/expression"
 	"sigs.k8s.io/kwok/pkg/utils/gotpl"
 	"sigs.k8s.io/kwok/pkg/utils/informer"
 	"sigs.k8s.io/kwok/pkg/utils/lifecycle"
@@ -278,13 +277,14 @@ func (c *NodeController) preprocess(ctx context.Context, node *corev1.Node) erro
 		}
 	}
 
-	data, err := expression.ToJSONStandard(node)
-	if err != nil {
-		return err
-	}
-
 	lc := c.lifecycle.Get()
-	stage, err := lc.Match(ctx, node.Labels, node.Annotations, data)
+
+	event := &lifecycle.Event{
+		Labels:      node.Labels,
+		Annotations: node.Annotations,
+		Data:        node,
+	}
+	stage, err := lc.Match(ctx, event)
 	if err != nil {
 		return fmt.Errorf("stage match: %w", err)
 	}
@@ -296,8 +296,13 @@ func (c *NodeController) preprocess(ctx context.Context, node *corev1.Node) erro
 	}
 
 	now := c.clock.Now()
-	delay, _ := stage.Delay(ctx, data, now)
-
+	delay, _, err := stage.Delay(ctx, event, now)
+	if err != nil {
+		logger.Warn("Failed to get delay",
+			"stage", stage.Name(),
+			"err", err,
+		)
+	}
 	if delay != 0 {
 		stageName := stage.Name()
 		logger.Debug("Delayed play stage",
diff --git a/pkg/kwok/controllers/pod_controller.go b/pkg/kwok/controllers/pod_controller.go
index 4c3588d7..1683d836 100644
--- a/pkg/kwok/controllers/pod_controller.go
+++ b/pkg/kwok/controllers/pod_controller.go
@@ -32,7 +32,6 @@ import (
 	"sigs.k8s.io/kwok/pkg/config/resources"
 	"sigs.k8s.io/kwok/pkg/kwok/cni"
 	"sigs.k8s.io/kwok/pkg/log"
-	"sigs.k8s.io/kwok/pkg/utils/expression"
 	"sigs.k8s.io/kwok/pkg/utils/gotpl"
 	"sigs.k8s.io/kwok/pkg/utils/informer"
 	"sigs.k8s.io/kwok/pkg/utils/lifecycle"
@@ -213,13 +212,14 @@ func (c *PodController) preprocess(ctx context.Context, pod *corev1.Pod) error {
 		}
 	}
 
-	data, err := expression.ToJSONStandard(pod)
-	if err != nil {
-		return err
+	event := &lifecycle.Event{
+		Labels:      pod.Labels,
+		Annotations: pod.Annotations,
+		Data:        pod,
 	}
 
 	lc := c.lifecycle.Get()
-	stage, err := lc.Match(ctx, pod.Labels, pod.Annotations, data)
+	stage, err := lc.Match(ctx, event)
 	if err != nil {
 		return fmt.Errorf("stage match: %w", err)
 	}
@@ -231,8 +231,13 @@ func (c *PodController) preprocess(ctx context.Context, pod *corev1.Pod) error {
 	}
 
 	now := c.clock.Now()
-	delay, _ := stage.Delay(ctx, data, now)
-
+	delay, _, err := stage.Delay(ctx, event, now)
+	if err != nil {
+		logger.Warn("Failed to get delay",
+			"stage", stage.Name(),
+			"err", err,
+		)
+	}
 	if delay != 0 {
 		stageName := stage.Name()
 		logger.Debug("Delayed play stage",
diff --git a/pkg/kwok/controllers/stage_controller.go b/pkg/kwok/controllers/stage_controller.go
index a74847a2..f31b0aa3 100644
--- a/pkg/kwok/controllers/stage_controller.go
+++ b/pkg/kwok/controllers/stage_controller.go
@@ -36,7 +36,6 @@ import (
 	"sigs.k8s.io/kwok/pkg/config/resources"
 	"sigs.k8s.io/kwok/pkg/log"
 	"sigs.k8s.io/kwok/pkg/utils/client"
-	"sigs.k8s.io/kwok/pkg/utils/expression"
 	"sigs.k8s.io/kwok/pkg/utils/gotpl"
 	"sigs.k8s.io/kwok/pkg/utils/informer"
 	"sigs.k8s.io/kwok/pkg/utils/lifecycle"
@@ -190,13 +189,14 @@ func (c *StageController) preprocess(ctx context.Context, resource *unstructured
 		}
 	}
 
-	data, err := expression.ToJSONStandard(resource)
-	if err != nil {
-		return err
+	event := &lifecycle.Event{
+		Labels:      resource.GetLabels(),
+		Annotations: resource.GetAnnotations(),
+		Data:        resource,
 	}
 
 	lc := c.lifecycle.Get()
-	stage, err := lc.Match(ctx, resource.GetLabels(), resource.GetAnnotations(), data)
+	stage, err := lc.Match(ctx, event)
 	if err != nil {
 		return fmt.Errorf("stage match: %w", err)
 	}
@@ -208,8 +208,13 @@ func (c *StageController) preprocess(ctx context.Context, resource *unstructured
 	}
 
 	now := c.clock.Now()
-	delay, _ := stage.Delay(ctx, data, now)
-
+	delay, _, err := stage.Delay(ctx, event, now)
+	if err != nil {
+		logger.Warn("Failed to get delay",
+			"stage", stage.Name(),
+			"err", err,
+		)
+	}
 	if delay != 0 {
 		stageName := stage.Name()
 		logger.Debug("Delayed play stage",
diff --git a/pkg/kwok/controllers/stages_manager.go b/pkg/kwok/controllers/stages_manager.go
index 09b62e42..ff763397 100644
--- a/pkg/kwok/controllers/stages_manager.go
+++ b/pkg/kwok/controllers/stages_manager.go
@@ -90,12 +90,11 @@ func (c *StagesManager) manage(ctx context.Context) {
 					return nil, false
 				}
 
-				lifecycleStage, err := lifecycle.NewStage(stage)
-				if err != nil {
-					logger.Error("failed to create lifecycle stage", err, "ref", ref)
+				lc := lifecycle.NewStage(stage)
+				if lc == nil {
 					return nil, false
 				}
-				return lifecycleStage, true
+				return lc, true
 			})
 		})
 
diff --git a/pkg/tools/stage/stage.go b/pkg/tools/stage/stage.go
index a528bfa2..e2a9743f 100644
--- a/pkg/tools/stage/stage.go
+++ b/pkg/tools/stage/stage.go
@@ -65,14 +65,20 @@ func TestingStages(ctx context.Context, target any, stages []*internalversion.St
 		return nil, err
 	}
 
-	lcstages, err := lc.ListAllPossible(ctx, testTarget.GetLabels(), testTarget.GetAnnotations(), testTarget)
+	event := &lifecycle.Event{
+		Labels:      testTarget.GetLabels(),
+		Annotations: testTarget.GetAnnotations(),
+		Data:        testTarget,
+	}
+
+	lcstages, err := lc.ListAllPossible(ctx, event)
 	if err != nil {
 		return nil, err
 	}
 
 	out := []any{}
 	for _, stage := range lcstages {
-		o, err := testingStage(ctx, testTarget, stage)
+		o, err := testingStage(ctx, testTarget, event, stage)
 		if err != nil {
 			return nil, err
 		}
@@ -86,17 +92,23 @@ func TestingStages(ctx context.Context, target any, stages []*internalversion.St
 
 var now = time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
 
-func testingStage(ctx context.Context, testTarget obj, stage *lifecycle.Stage) (any, error) {
+func testingStage(ctx context.Context, testTarget obj, event *lifecycle.Event, stage *lifecycle.Stage) (any, error) {
 	meta := map[string]any{
 		"stage": stage.Name(),
 	}
 
-	delay, ok := stage.DelayRangePossible(ctx, testTarget, now)
+	delay, ok, err := stage.DelayRangePossible(ctx, event, now)
+	if err != nil {
+		return nil, err
+	}
 	if ok {
 		meta["delay"] = delay
 	}
 
-	weight, ok := stage.Weight(ctx, testTarget)
+	weight, ok, err := stage.Weight(ctx, event)
+	if err != nil {
+		return nil, err
+	}
 	if ok {
 		meta["weight"] = weight
 	}
diff --git a/pkg/utils/cel/environment.go b/pkg/utils/cel/environment.go
index acbf1623..aa659b3b 100644
--- a/pkg/utils/cel/environment.go
+++ b/pkg/utils/cel/environment.go
@@ -19,6 +19,7 @@ package cel
 import (
 	"fmt"
 	"sync"
+	"time"
 
 	"github.com/google/cel-go/cel"
 	"github.com/google/cel-go/common/types"
@@ -132,7 +133,7 @@ func AsFloat64(refVal ref.Val) (float64, error) {
 	case Quantity:
 		return v.Quantity.AsApproximateFloat64(), nil
 	default:
-		return 0, fmt.Errorf("unsupported type: %T", v)
+		return 0, fmt.Errorf("cel.AsFloat64 unsupported type: %T", refVal)
 	}
 }
 
@@ -140,7 +141,55 @@ func AsFloat64(refVal ref.Val) (float64, error) {
 func AsString(refVal ref.Val) (string, error) {
 	v, ok := refVal.(types.String)
 	if !ok {
-		return "", fmt.Errorf("unsupported type: %T", v)
+		return "", fmt.Errorf("cel.AsString unsupported type: %T", refVal)
 	}
 	return string(v), nil
 }
+
+// AsBool returns the bool value of a ref.Val
+func AsBool(refVal ref.Val) (bool, error) {
+	v, ok := refVal.(types.Bool)
+	if !ok {
+		return false, fmt.Errorf("cel.AsBool unsupported type: %T", refVal)
+	}
+	return bool(v), nil
+}
+
+// AsDuration returns the time.Duration value of a ref.Val
+func AsDuration(refVal ref.Val) (time.Duration, error) {
+	switch v := refVal.(type) {
+	case types.String:
+		return time.ParseDuration(string(v))
+	case types.Duration:
+		return v.Duration, nil
+	case types.Int:
+		return time.Duration(v), nil
+	case types.Double:
+		return time.Duration(v), nil
+	case types.Uint:
+		return time.Duration(v), nil
+	default:
+		return 0, fmt.Errorf("cel.AsDuration unsupported type: %T", refVal)
+	}
+}
+
+// AsInt64 returns the int64 value of a ref.Val
+func AsInt64(refVal ref.Val) (int64, error) {
+	switch v := refVal.(type) {
+	case types.Duration:
+		return int64(v.Duration), nil
+	case types.Int:
+		return int64(v), nil
+	case types.Double:
+		return int64(v), nil
+	case types.Uint:
+		return int64(v), nil
+	case types.Bool:
+		if v {
+			return 1, nil
+		}
+		return 0, nil
+	default:
+		return 0, fmt.Errorf("cel.AsInt64 unsupported type: %T", refVal)
+	}
+}
diff --git a/pkg/utils/expression/value_duration_from.go b/pkg/utils/expression/value_duration_from.go
deleted file mode 100644
index 648c6b60..00000000
--- a/pkg/utils/expression/value_duration_from.go
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
-Copyright 2022 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 expression
-
-import (
-	"context"
-	"time"
-)
-
-// DurationGetter is a interface that can be used to get a time.Duration value.
-type DurationGetter interface {
-	// Get returns a duration value.
-	Get(ctx context.Context, v interface{}, now time.Time) (time.Duration, bool)
-}
-
-type durationFrom struct {
-	value *time.Duration
-	query *Query
-}
-
-// NewDurationFrom returns a new DurationGetter.
-func NewDurationFrom(value *time.Duration, src *string) (DurationGetter, error) {
-	if value == nil && src == nil {
-		return durationNoop{}, nil
-	}
-	if src == nil {
-		return duration(*value), nil
-	}
-	query, err := NewQuery(*src)
-	if err != nil {
-		return nil, err
-	}
-	return &durationFrom{
-		value: value,
-		query: query,
-	}, nil
-}
-
-func (d *durationFrom) Get(ctx context.Context, v interface{}, now time.Time) (time.Duration, bool) {
-	out, err := d.query.Execute(ctx, v)
-	if err != nil {
-		return 0, false
-	}
-	if len(out) == 0 {
-		if d.value != nil {
-			return *d.value, true
-		}
-		return 0, false
-	}
-	if t, ok := out[0].(string); ok {
-		if t == "" {
-			return 0, false
-		}
-		ti, err := time.Parse(time.RFC3339Nano, t)
-		if err == nil {
-			d := ti.Sub(now)
-			return d, true
-		}
-		du, err := time.ParseDuration(t)
-		if err == nil {
-			return du, true
-		}
-	}
-	return 0, false
-}
-
-type durationNoop struct {
-}
-
-func (durationNoop) Get(ctx context.Context, v interface{}, now time.Time) (time.Duration, bool) {
-	return 0, false
-}
-
-type duration int64
-
-func (i duration) Get(ctx context.Context, v interface{}, now time.Time) (time.Duration, bool) {
-	return time.Duration(i), true
-}
diff --git a/pkg/utils/expression/value_int_from.go b/pkg/utils/expression/value_int_from.go
deleted file mode 100644
index dd2bae3d..00000000
--- a/pkg/utils/expression/value_int_from.go
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
-Copyright 2024 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 expression
-
-import (
-	"context"
-	"strconv"
-)
-
-// IntGetter is a interface that can be used to get a int64 value.
-type IntGetter interface {
-	// Get returns a int64 value.
-	Get(ctx context.Context, v interface{}) (int64, bool)
-}
-
-type int64From struct {
-	value *int64
-	query *Query
-}
-
-// NewIntFrom returns a new IntGetter.
-func NewIntFrom(value *int64, src *string) (IntGetter, error) {
-	if value == nil && src == nil {
-		return int64Noop{}, nil
-	}
-	if src == nil {
-		return intWrapper(*value), nil
-	}
-	query, err := NewQuery(*src)
-	if err != nil {
-		return nil, err
-	}
-	return &int64From{
-		value: value,
-		query: query,
-	}, nil
-}
-
-func (d *int64From) Get(ctx context.Context, v interface{}) (int64, bool) {
-	out, err := d.query.Execute(ctx, v)
-	if err != nil {
-		return 0, false
-	}
-	if len(out) == 0 {
-		if d.value != nil {
-			return *d.value, true
-		}
-		return 0, false
-	}
-	switch t := out[0].(type) {
-	case string:
-		if t == "" {
-			return 0, false
-		}
-		n, err := strconv.ParseInt(t, 0, 0)
-		if err == nil {
-			return n, true
-		}
-		return 0, false
-	case float64: // TODO: don't use float
-		return int64(t), true
-	}
-	if d.value != nil {
-		return *d.value, true
-	}
-	return 0, false
-}
-
-type int64Noop struct {
-}
-
-func (int64Noop) Get(ctx context.Context, v interface{}) (int64, bool) {
-	return 0, false
-}
-
-type intWrapper int64
-
-func (i intWrapper) Get(ctx context.Context, v interface{}) (int64, bool) {
-	return int64(i), true
-}
diff --git a/pkg/utils/lifecycle/lifecycle.go b/pkg/utils/lifecycle/lifecycle.go
index 15c1fd33..6750edaf 100644
--- a/pkg/utils/lifecycle/lifecycle.go
+++ b/pkg/utils/lifecycle/lifecycle.go
@@ -19,12 +19,17 @@ package lifecycle
 import (
 	"context"
 	"fmt"
+	"maps"
 	"math/rand"
+	"reflect"
+	"slices"
 	"time"
 
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/labels"
 
 	"sigs.k8s.io/kwok/pkg/apis/internalversion"
+	"sigs.k8s.io/kwok/pkg/utils/cel"
 	"sigs.k8s.io/kwok/pkg/utils/expression"
 	"sigs.k8s.io/kwok/pkg/utils/format"
 )
@@ -33,10 +38,7 @@ import (
 func NewLifecycle(stages []*internalversion.Stage) (Lifecycle, error) {
 	lcs := Lifecycle{}
 	for _, stage := range stages {
-		lc, err := NewStage(stage)
-		if err != nil {
-			return nil, fmt.Errorf("lifecycle stage: %w", err)
-		}
+		lc := NewStage(stage)
 		if lc == nil {
 			continue
 		}
@@ -48,10 +50,10 @@ func NewLifecycle(stages []*internalversion.Stage) (Lifecycle, error) {
 // Lifecycle is a list of lifecycle stage.
 type Lifecycle []*Stage
 
-func (s Lifecycle) match(label, annotation labels.Set, data interface{}) ([]*Stage, error) {
+func (s Lifecycle) match(ctx context.Context, event *Event) ([]*Stage, error) {
 	out := []*Stage{}
 	for _, stage := range s {
-		ok, err := stage.match(label, annotation, data)
+		ok, err := stage.match(ctx, event)
 		if err != nil {
 			return nil, err
 		}
@@ -63,12 +65,8 @@ func (s Lifecycle) match(label, annotation labels.Set, data interface{}) ([]*Sta
 }
 
 // ListAllPossible returns all possible stages.
-func (s Lifecycle) ListAllPossible(ctx context.Context, label, annotation labels.Set, data interface{}) ([]*Stage, error) {
-	data, err := expression.ToJSONStandard(data)
-	if err != nil {
-		return nil, err
-	}
-	stages, err := s.match(label, annotation, data)
+func (s Lifecycle) ListAllPossible(ctx context.Context, event *Event) ([]*Stage, error) {
+	stages, err := s.match(ctx, event)
 	if err != nil {
 		return nil, err
 	}
@@ -83,7 +81,10 @@ func (s Lifecycle) ListAllPossible(ctx context.Context, label, annotation labels
 	var totalWeights int64
 	var countError int
 	for _, stage := range stages {
-		w, ok := stage.Weight(ctx, data)
+		w, ok, err := stage.Weight(ctx, event)
+		if err != nil {
+			return nil, err
+		}
 		if ok {
 			totalWeights += w
 			weights = append(weights, w)
@@ -122,12 +123,8 @@ func (s Lifecycle) ListAllPossible(ctx context.Context, label, annotation labels
 }
 
 // Match returns matched stage.
-func (s Lifecycle) Match(ctx context.Context, label, annotation labels.Set, data interface{}) (*Stage, error) {
-	data, err := expression.ToJSONStandard(data)
-	if err != nil {
-		return nil, err
-	}
-	stages, err := s.match(label, annotation, data)
+func (s Lifecycle) Match(ctx context.Context, event *Event) (*Stage, error) {
+	stages, err := s.match(ctx, event)
 	if err != nil {
 		return nil, err
 	}
@@ -142,7 +139,10 @@ func (s Lifecycle) Match(ctx context.Context, label, annotation labels.Set, data
 	var totalWeights int64
 	var countError int
 	for _, stage := range stages {
-		w, ok := stage.Weight(ctx, data)
+		w, ok, err := stage.Weight(ctx, event)
+		if err != nil {
+			return nil, err
+		}
 		if ok {
 			totalWeights += w
 			weights = append(weights, w)
@@ -191,79 +191,117 @@ func (s Lifecycle) Match(ctx context.Context, label, annotation labels.Set, data
 }
 
 // NewStage returns a new Stage.
-func NewStage(s *internalversion.Stage) (*Stage, error) {
+func NewStage(s *internalversion.Stage) *Stage {
+	if s.Spec.Selector == nil {
+		return nil
+	}
+
 	stage := &Stage{
-		name: s.Name,
+		name:   s.Name,
+		config: s,
 	}
-	selector := s.Spec.Selector
-	if selector == nil {
-		return nil, nil
+
+	return stage
+}
+
+func (s *Stage) init(event *Event) error {
+	if s.env != nil {
+		return nil
+	}
+	types := slices.Clone(cel.DefaultTypes)
+	conversions := slices.Clone(cel.DefaultConversions)
+	funcs := maps.Clone(cel.DefaultFuncs)
+	methods := maps.Clone(cel.FuncsToMethods(cel.DefaultFuncs))
+	env, err := cel.NewEnvironment(cel.EnvironmentConfig{
+		Types:       types,
+		Conversions: conversions,
+		Methods:     methods,
+		Funcs:       funcs,
+		Vars:        event.toCELStandardTypeOnly(),
+	})
+	if err != nil {
+		return err
 	}
+	s.env = env
+
+	selector := s.config.Spec.Selector
+	config := s.config
 
 	if selector.MatchLabels != nil {
-		stage.matchLabels = labels.SelectorFromSet(selector.MatchLabels)
+		s.matchLabels = labels.SelectorFromSet(selector.MatchLabels)
 	}
 	if selector.MatchAnnotations != nil {
-		stage.matchAnnotations = labels.SelectorFromSet(selector.MatchAnnotations)
+		s.matchAnnotations = labels.SelectorFromSet(selector.MatchAnnotations)
 	}
 	if selector.MatchExpressions != nil {
 		for _, express := range selector.MatchExpressions {
-			requirement, err := expression.NewRequirement(express.Key, express.Operator, express.Values)
-			if err != nil {
-				return nil, err
+			switch {
+			case express.JQ != nil:
+				requirement, err := expression.NewRequirement(express.JQ.Key, express.JQ.Operator, express.JQ.Values)
+				if err != nil {
+					return err
+				}
+				s.matchExpressions = append(s.matchExpressions, requirement)
+			case express.CEL != nil:
+				program, err := env.Compile(express.CEL.Expression)
+				if err != nil {
+					return err
+				}
+				s.matchConditions = append(s.matchConditions, program)
+			default:
+				return fmt.Errorf("invalid expression")
 			}
-			stage.matchExpressions = append(stage.matchExpressions, requirement)
 		}
 	}
 
-	stage.next = &s.Spec.Next
-	if delay := s.Spec.Delay; delay != nil {
-		var durationFrom *string
+	s.next = &config.Spec.Next
+	if delay := config.Spec.Delay; delay != nil {
+		var durationFrom *internalversion.ExpressionFrom
 		if delay.DurationFrom != nil {
-			durationFrom = &delay.DurationFrom.ExpressionFrom
+			durationFrom = delay.DurationFrom
 		}
 		var delayDuration time.Duration
 		if delay.DurationMilliseconds != nil {
 			delayDuration = time.Duration(*delay.DurationMilliseconds) * time.Millisecond
 		}
-		duration, err := expression.NewDurationFrom(&delayDuration, durationFrom)
+		duration, err := newDurationFrom(&delayDuration, env, durationFrom)
 		if err != nil {
-			return nil, err
+			return err
 		}
-		stage.duration = duration
+		s.duration = duration
 
 		if delay.JitterDurationMilliseconds != nil || delay.JitterDurationFrom != nil {
-			var jitterDurationFrom *string
-			if delay.JitterDurationFrom != nil {
-				jitterDurationFrom = &delay.JitterDurationFrom.ExpressionFrom
+			var jitterDurationFrom *internalversion.ExpressionFrom
+			if delay.JitterDurationFrom != nil && delay.JitterDurationFrom.JQ != nil {
+				jitterDurationFrom = delay.JitterDurationFrom
 			}
 			var jitterDuration *time.Duration
 			if delay.JitterDurationMilliseconds != nil {
 				jitterDuration = format.Ptr(time.Duration(*delay.JitterDurationMilliseconds) * time.Millisecond)
 			}
-			jitterDurationGetter, err := expression.NewDurationFrom(jitterDuration, jitterDurationFrom)
+			jitterDurationGetter, err := newDurationFrom(jitterDuration, env, jitterDurationFrom)
 			if err != nil {
-				return nil, err
+				return err
 			}
-			stage.jitterDuration = jitterDurationGetter
+			s.jitterDuration = jitterDurationGetter
 		}
 	}
 
-	var weightFrom *string
-	if wf := s.Spec.WeightFrom; wf != nil {
-		weightFrom = &wf.ExpressionFrom
+	var weightFrom *internalversion.ExpressionFrom
+	if config.Spec.WeightFrom != nil {
+		weightFrom = config.Spec.WeightFrom
 	}
 
-	weightGetter, err := expression.NewIntFrom(format.Ptr[int64](int64(s.Spec.Weight)), weightFrom)
+	weightGetter, err := newInt64From(format.Ptr[int64](int64(config.Spec.Weight)), env, weightFrom)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	stage.weight = weightGetter
+	s.weight = weightGetter
 
-	stage.immediateNextStage = s.Spec.ImmediateNextStage
+	s.immediateNextStage = config.Spec.ImmediateNextStage
 
-	return stage, nil
+	return nil
 }
 
 // Stage is a resource lifecycle stage manager
@@ -272,31 +310,103 @@ type Stage struct {
 	matchLabels      labels.Selector
 	matchAnnotations labels.Selector
 	matchExpressions []*expression.Requirement
+	matchConditions  []cel.Program
 
-	weight expression.IntGetter
+	weight int64Getter
 	next   *internalversion.StageNext
 
-	duration       expression.DurationGetter
-	jitterDuration expression.DurationGetter
+	duration       durationGetter
+	jitterDuration durationGetter
 
 	immediateNextStage bool
+
+	config *internalversion.Stage
+
+	env *cel.Environment
+}
+
+// Event represents a lifecycle event that can be matched against stage conditions
+// It contains the resource's labels, annotations, raw data, and a cached JSON representation
+type Event struct {
+	Labels       labels.Set
+	Annotations  labels.Set
+	Data         any
+	jsonStandard any
+}
+
+func (m *Event) toJSONStandard() any {
+	if m.jsonStandard != nil {
+		return m.jsonStandard
+	}
+	j, err := expression.ToJSONStandard(m.Data)
+	if err != nil {
+		// This should never happen as all resources should be convertible to JSON
+		panic(fmt.Errorf("failed to convert data to JSON standard: %v, error: %w", m.Data, err))
+	}
+	m.jsonStandard = j
+	return j
 }
 
-func (s *Stage) match(label, annotation labels.Set, jsonStandard interface{}) (bool, error) {
+func (m *Event) toCELStandard() map[string]any {
+	if v, ok := m.Data.(*unstructured.Unstructured); ok {
+		return map[string]any{
+			"self": v.Object,
+		}
+	}
+	return map[string]any{
+		"self": m.Data,
+	}
+}
+
+func (m *Event) toCELStandardTypeOnly() map[string]any {
+	if _, ok := m.Data.(*unstructured.Unstructured); ok {
+		return map[string]any{
+			"self": map[string]any{},
+		}
+	}
+
+	return map[string]any{
+		"self": reflect.Zero(reflect.TypeOf(m.Data)).Interface(),
+	}
+}
+
+func (s *Stage) match(ctx context.Context, event *Event) (bool, error) {
+	err := s.init(event)
+	if err != nil {
+		return false, err
+	}
 	if s.matchLabels != nil {
-		if !s.matchLabels.Matches(label) {
+		if !s.matchLabels.Matches(event.Labels) {
 			return false, nil
 		}
 	}
 	if s.matchAnnotations != nil {
-		if !s.matchAnnotations.Matches(annotation) {
+		if !s.matchAnnotations.Matches(event.Annotations) {
 			return false, nil
 		}
 	}
 
 	if s.matchExpressions != nil {
+		v := event.toJSONStandard()
 		for _, requirement := range s.matchExpressions {
-			ok, err := requirement.Matches(context.Background(), jsonStandard)
+			ok, err := requirement.Matches(ctx, v)
+			if err != nil {
+				return false, err
+			}
+			if !ok {
+				return false, nil
+			}
+		}
+	}
+
+	if s.matchConditions != nil {
+		v := event.toCELStandard()
+		for _, program := range s.matchConditions {
+			val, _, err := program.ContextEval(ctx, v)
+			if err != nil {
+				return false, err
+			}
+			ok, err := cel.AsBool(val)
 			if err != nil {
 				return false, err
 			}
@@ -305,65 +415,78 @@ func (s *Stage) match(label, annotation labels.Set, jsonStandard interface{}) (b
 			}
 		}
 	}
+
 	return true, nil
 }
 
 // Delay returns the delay duration of the stage.
 // It's not a constant value, it can be a random value.
-func (s *Stage) Delay(ctx context.Context, v interface{}, now time.Time) (time.Duration, bool) {
+func (s *Stage) Delay(ctx context.Context, event *Event, now time.Time) (time.Duration, bool, error) {
 	if s.duration == nil {
-		return 0, false
+		return 0, false, nil
 	}
 
-	duration, ok := s.duration.Get(ctx, v, now)
+	duration, ok, err := s.duration.Get(ctx, event, now)
+	if err != nil {
+		return 0, false, err
+	}
 	if !ok {
-		return 0, false
+		return 0, false, nil
 	}
 
 	if s.jitterDuration == nil {
-		return duration, true
+		return duration, true, nil
 	}
 
-	jitterDuration, ok := s.jitterDuration.Get(ctx, v, now)
+	jitterDuration, ok, err := s.jitterDuration.Get(ctx, event, now)
+	if err != nil {
+		return 0, false, err
+	}
 	if !ok {
-		return duration, true
+		return duration, true, nil
 	}
 
 	if jitterDuration <= duration {
-		return jitterDuration, true
+		return jitterDuration, true, nil
 	}
 
 	//nolint:gosec
 	duration += time.Duration(rand.Int63n(int64(jitterDuration - duration)))
 
-	return duration, true
+	return duration, true, nil
 }
 
 // DelayRangePossible returns possible range of delay.
-func (s *Stage) DelayRangePossible(ctx context.Context, v interface{}, now time.Time) ([]time.Duration, bool) {
+func (s *Stage) DelayRangePossible(ctx context.Context, event *Event, now time.Time) ([]time.Duration, bool, error) {
 	if s.duration == nil {
-		return nil, false
+		return nil, false, nil
 	}
 
-	duration, ok := s.duration.Get(ctx, v, now)
+	duration, ok, err := s.duration.Get(ctx, event, now)
+	if err != nil {
+		return nil, false, err
+	}
 	if !ok {
-		return nil, false
+		return nil, false, nil
 	}
 
 	if s.jitterDuration == nil {
-		return []time.Duration{duration}, true
+		return []time.Duration{duration}, true, nil
 	}
 
-	jitterDuration, ok := s.jitterDuration.Get(ctx, v, now)
+	jitterDuration, ok, err := s.jitterDuration.Get(ctx, event, now)
+	if err != nil {
+		return nil, false, err
+	}
 	if !ok {
-		return []time.Duration{duration}, true
+		return []time.Duration{duration}, true, nil
 	}
 
 	if jitterDuration <= duration {
-		return []time.Duration{jitterDuration}, true
+		return []time.Duration{jitterDuration}, true, nil
 	}
 
-	return []time.Duration{duration, jitterDuration}, true
+	return []time.Duration{duration, jitterDuration}, true, nil
 }
 
 // Next returns the next of the stage.
@@ -382,6 +505,6 @@ func (s *Stage) ImmediateNextStage() bool {
 }
 
 // Weight returns the weight of the stage.
-func (s *Stage) Weight(ctx context.Context, v interface{}) (int64, bool) {
-	return s.weight.Get(ctx, v)
+func (s *Stage) Weight(ctx context.Context, event *Event) (int64, bool, error) {
+	return s.weight.Get(ctx, event)
 }
diff --git a/pkg/utils/lifecycle/value_duration_from.go b/pkg/utils/lifecycle/value_duration_from.go
new file mode 100644
index 00000000..2d9b3abd
--- /dev/null
+++ b/pkg/utils/lifecycle/value_duration_from.go
@@ -0,0 +1,130 @@
+/*
+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 lifecycle
+
+import (
+	"context"
+	"time"
+
+	"sigs.k8s.io/kwok/pkg/apis/internalversion"
+	"sigs.k8s.io/kwok/pkg/utils/cel"
+	"sigs.k8s.io/kwok/pkg/utils/expression"
+)
+
+// durationGetter is a interface that can be used to get a time.Duration value.
+type durationGetter interface {
+	// Get returns a duration value.
+	Get(ctx context.Context, event *Event, now time.Time) (time.Duration, bool, error)
+}
+
+type durationFrom struct {
+	value   *time.Duration
+	query   *expression.Query
+	program cel.Program
+}
+
+// newDurationFrom returns a new durationGetter.
+func newDurationFrom(value *time.Duration, env *cel.Environment, src *internalversion.ExpressionFrom) (durationGetter, error) {
+	if value == nil && src == nil {
+		return durationNoop{}, nil
+	}
+	if src == nil {
+		return duration(*value), nil
+	}
+
+	d := &durationFrom{
+		value: value,
+	}
+
+	if src.CEL != nil {
+		program, err := env.Compile(src.CEL.Expression)
+		if err != nil {
+			return nil, err
+		}
+		d.program = program
+	}
+
+	if src.JQ != nil {
+		query, err := expression.NewQuery(src.JQ.Expression)
+		if err != nil {
+			return nil, err
+		}
+		d.query = query
+	}
+	return d, nil
+}
+
+func (d *durationFrom) Get(ctx context.Context, event *Event, now time.Time) (time.Duration, bool, error) {
+	if d.program != nil {
+		out, _, err := d.program.Eval(event.toCELStandard())
+		if err != nil {
+			return 0, false, err
+		}
+
+		n, err := cel.AsDuration(out)
+		if err != nil {
+			return 0, false, err
+		}
+
+		return n, true, nil
+	}
+
+	if d.query != nil {
+		out, err := d.query.Execute(ctx, event.toJSONStandard())
+		if err != nil {
+			return 0, false, err
+		}
+		if len(out) == 0 {
+			return d.defaultValue()
+		}
+		if t, ok := out[0].(string); ok {
+			if t == "" {
+				return 0, false, nil
+			}
+			ti, err := time.Parse(time.RFC3339Nano, t)
+			if err == nil {
+				d := ti.Sub(now)
+				return d, true, nil
+			}
+			du, err := time.ParseDuration(t)
+			if err == nil {
+				return du, true, nil
+			}
+		}
+	}
+	return d.defaultValue()
+}
+
+func (d *durationFrom) defaultValue() (time.Duration, bool, error) {
+	if d.value != nil {
+		return *d.value, true, nil
+	}
+	return 0, false, nil
+}
+
+type durationNoop struct {
+}
+
+func (durationNoop) Get(ctx context.Context, event *Event, now time.Time) (time.Duration, bool, error) {
+	return 0, false, nil
+}
+
+type duration int64
+
+func (i duration) Get(ctx context.Context, event *Event, now time.Time) (time.Duration, bool, error) {
+	return time.Duration(i), true, nil
+}
diff --git a/pkg/utils/lifecycle/value_int_from.go b/pkg/utils/lifecycle/value_int_from.go
new file mode 100644
index 00000000..a52e2649
--- /dev/null
+++ b/pkg/utils/lifecycle/value_int_from.go
@@ -0,0 +1,129 @@
+/*
+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 lifecycle
+
+import (
+	"context"
+	"strconv"
+
+	"sigs.k8s.io/kwok/pkg/apis/internalversion"
+	"sigs.k8s.io/kwok/pkg/utils/cel"
+	"sigs.k8s.io/kwok/pkg/utils/expression"
+)
+
+// int64Getter is a interface that can be used to get a int64 value.
+type int64Getter interface {
+	// Get returns a int64 value.
+	Get(ctx context.Context, event *Event) (int64, bool, error)
+}
+
+type int64From struct {
+	value   *int64
+	query   *expression.Query
+	program cel.Program
+}
+
+// newInt64From returns a new int64Getter.
+func newInt64From(value *int64, env *cel.Environment, src *internalversion.ExpressionFrom) (int64Getter, error) {
+	if value == nil && src == nil {
+		return int64Noop{}, nil
+	}
+	if src == nil {
+		return intWrapper(*value), nil
+	}
+
+	d := &int64From{
+		value: value,
+	}
+	if src.CEL != nil {
+		program, err := env.Compile(src.CEL.Expression)
+		if err != nil {
+			return nil, err
+		}
+		d.program = program
+	}
+
+	if src.JQ != nil {
+		query, err := expression.NewQuery(src.JQ.Expression)
+		if err != nil {
+			return nil, err
+		}
+		d.query = query
+	}
+	return d, nil
+}
+
+func (d *int64From) Get(ctx context.Context, event *Event) (int64, bool, error) {
+	if d.program != nil {
+		out, _, err := d.program.Eval(event.toCELStandard())
+		if err != nil {
+			return 0, false, err
+		}
+
+		n, err := cel.AsInt64(out)
+		if err != nil {
+			return 0, false, err
+		}
+
+		return n, true, nil
+	}
+
+	if d.query != nil {
+		out, err := d.query.Execute(ctx, event.toJSONStandard())
+		if err != nil {
+			return 0, false, err
+		}
+		if len(out) == 0 {
+			return d.defaultValue()
+		}
+		switch t := out[0].(type) {
+		case string:
+			if t == "" {
+				return 0, false, nil
+			}
+			n, err := strconv.ParseInt(t, 0, 0)
+			if err == nil {
+				return n, true, nil
+			}
+			return 0, false, nil
+		case float64: // TODO: don't use float
+			return int64(t), true, nil
+		}
+	}
+
+	return d.defaultValue()
+}
+
+func (d *int64From) defaultValue() (int64, bool, error) {
+	if d.value != nil {
+		return *d.value, true, nil
+	}
+	return 0, false, nil
+}
+
+type int64Noop struct {
+}
+
+func (int64Noop) Get(ctx context.Context, event *Event) (int64, bool, error) {
+	return 0, false, nil
+}
+
+type intWrapper int64
+
+func (i intWrapper) Get(ctx context.Context, event *Event) (int64, bool, error) {
+	return int64(i), true, nil
+}
diff --git a/site/content/en/docs/generated/apis.md b/site/content/en/docs/generated/apis.md
index 0ea86058..82f37782 100644
--- a/site/content/en/docs/generated/apis.md
+++ b/site/content/en/docs/generated/apis.md
@@ -1631,8 +1631,8 @@ a random stage will be matched as the next stage based on the weight.</p>
 <td>
 <code>weightFrom</code>
 <em>
-<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFromSource">
-ExpressionFromSource
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFrom">
+ExpressionFrom
 </a>
 </em>
 </td>
@@ -4990,9 +4990,43 @@ SecurityContext
 </tr>
 </tbody>
 </table>
-<h3 id="kwok.x-k8s.io/v1alpha1.ExpressionFromSource">
-ExpressionFromSource
-<a href="#kwok.x-k8s.io%2fv1alpha1.ExpressionFromSource"> #</a>
+<h3 id="kwok.x-k8s.io/v1alpha1.ExpressionCEL">
+ExpressionCEL
+<a href="#kwok.x-k8s.io%2fv1alpha1.ExpressionCEL"> #</a>
+</h3>
+<p>
+<em>Appears on: </em>
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFrom">ExpressionFrom</a>
+, 
+<a href="#kwok.x-k8s.io/v1alpha1.MatchExpression">MatchExpression</a>
+</p>
+<p>
+<p>ExpressionCEL is the expression which will be evaluated by CEL.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>expression</code>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Expression represents the expression which will be evaluated by CEL.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="kwok.x-k8s.io/v1alpha1.ExpressionFrom">
+ExpressionFrom
+<a href="#kwok.x-k8s.io%2fv1alpha1.ExpressionFrom"> #</a>
 </h3>
 <p>
 <em>Appears on: </em>
@@ -5001,7 +5035,7 @@ ExpressionFromSource
 <a href="#kwok.x-k8s.io/v1alpha1.StageSpec">StageSpec</a>
 </p>
 <p>
-<p>ExpressionFromSource represents a source for the value of a from.</p>
+<p>ExpressionFrom represents a source for extracting values using expressions</p>
 </p>
 <table>
 <thead>
@@ -5013,13 +5047,72 @@ ExpressionFromSource
 <tbody>
 <tr>
 <td>
+<code>cel</code>
+<em>
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionCEL">
+ExpressionCEL
+</a>
+</em>
+</td>
+<td>
+<p>CEL is a Common Expression Language based expression for value extraction</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>jq</code>
+<em>
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionJQ">
+ExpressionJQ
+</a>
+</em>
+</td>
+<td>
+<p>JQ is a JSON Query based expression for value extraction</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>expressionFrom</code>
 <em>
 string
 </em>
 </td>
 <td>
-<p>ExpressionFrom is the expression used to get the value.</p>
+<p>ExpressionFrom is the expression used to get the value.
+Deprecated: Use JQ instead.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="kwok.x-k8s.io/v1alpha1.ExpressionJQ">
+ExpressionJQ
+<a href="#kwok.x-k8s.io%2fv1alpha1.ExpressionJQ"> #</a>
+</h3>
+<p>
+<em>Appears on: </em>
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFrom">ExpressionFrom</a>
+</p>
+<p>
+<p>ExpressionJQ is the expression which will be evaluated by JQ.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>expression</code>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Expression represents the expression which will be evaluated by JQ.</p>
 </td>
 </tr>
 </tbody>
@@ -5365,6 +5458,93 @@ LogsStatus
 </tr>
 </tbody>
 </table>
+<h3 id="kwok.x-k8s.io/v1alpha1.MatchExpression">
+MatchExpression
+<a href="#kwok.x-k8s.io%2fv1alpha1.MatchExpression"> #</a>
+</h3>
+<p>
+<em>Appears on: </em>
+<a href="#kwok.x-k8s.io/v1alpha1.StageSelector">StageSelector</a>
+</p>
+<p>
+<p>MatchExpression is a resource selector expression that must evaluate to true for a resource to be matched.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>cel</code>
+<em>
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionCEL">
+ExpressionCEL
+</a>
+</em>
+</td>
+<td>
+<p>CEL is a Common Expression Language based selector expression</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>jq</code>
+<em>
+<a href="#kwok.x-k8s.io/v1alpha1.SelectorJQ">
+SelectorJQ
+</a>
+</em>
+</td>
+<td>
+<p>JQ is a JSON Query based selector expression</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>key</code>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Key represents the expression which will be evaluated by JQ.
+Deprecated: Use JQ instead.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>operator</code>
+<em>
+<a href="#kwok.x-k8s.io/v1alpha1.SelectorOperator">
+SelectorOperator
+</a>
+</em>
+</td>
+<td>
+<p>Represents a scope&rsquo;s relationship to a set of values.
+Deprecated: Use JQ instead.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>values</code>
+<em>
+[]string
+</em>
+</td>
+<td>
+<p>An array of string values.
+If the operator is In, NotIn, Intersection or NotIntersection, the values array must be non-empty.
+If the operator is Exists or DoesNotExist, the values array must be empty.
+Deprecated: Use JQ instead.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="kwok.x-k8s.io/v1alpha1.MetricBucket">
 MetricBucket
 <a href="#kwok.x-k8s.io%2fv1alpha1.MetricBucket"> #</a>
@@ -5970,58 +6150,16 @@ int64
 </tr>
 </tbody>
 </table>
-<h3 id="kwok.x-k8s.io/v1alpha1.SelectorOperator">
-SelectorOperator
-(<code>string</code> alias)
-<a href="#kwok.x-k8s.io%2fv1alpha1.SelectorOperator"> #</a>
-</h3>
-<p>
-<em>Appears on: </em>
-<a href="#kwok.x-k8s.io/v1alpha1.SelectorRequirement">SelectorRequirement</a>
-</p>
-<p>
-<p>SelectorOperator is a label selector operator is the set of operators that can be used in a selector requirement.</p>
-</p>
-<table>
-<thead>
-<tr>
-<th>Value</th>
-<th>Description</th>
-</tr>
-</thead>
-<tbody>
-<tr>
-<td><code>&#34;DoesNotExist&#34;</code></td>
-<td><p>SelectorOpDoesNotExist is the negated existence operator.</p>
-</td>
-</tr>
-<tr>
-<td><code>&#34;Exists&#34;</code></td>
-<td><p>SelectorOpExists is the existence operator.</p>
-</td>
-</tr>
-<tr>
-<td><code>&#34;In&#34;</code></td>
-<td><p>SelectorOpIn is the set inclusion operator.</p>
-</td>
-</tr>
-<tr>
-<td><code>&#34;NotIn&#34;</code></td>
-<td><p>SelectorOpNotIn is the negated set inclusion operator.</p>
-</td>
-</tr>
-</tbody>
-</table>
-<h3 id="kwok.x-k8s.io/v1alpha1.SelectorRequirement">
-SelectorRequirement
-<a href="#kwok.x-k8s.io%2fv1alpha1.SelectorRequirement"> #</a>
+<h3 id="kwok.x-k8s.io/v1alpha1.SelectorJQ">
+SelectorJQ
+<a href="#kwok.x-k8s.io%2fv1alpha1.SelectorJQ"> #</a>
 </h3>
 <p>
 <em>Appears on: </em>
-<a href="#kwok.x-k8s.io/v1alpha1.StageSelector">StageSelector</a>
+<a href="#kwok.x-k8s.io/v1alpha1.MatchExpression">MatchExpression</a>
 </p>
 <p>
-<p>SelectorRequirement is a resource selector requirement is a selector that contains values, a key,
+<p>SelectorJQ is a resource selector requirement is a selector that contains values, a key,
 and an operator that relates the key and values.</p>
 </p>
 <table>
@@ -6040,7 +6178,7 @@ string
 </em>
 </td>
 <td>
-<p>The name of the scope that the selector applies to.</p>
+<p>Key represents the expression which will be evaluated by JQ.</p>
 </td>
 </tr>
 <tr>
@@ -6071,6 +6209,50 @@ If the operator is Exists or DoesNotExist, the values array must be empty.</p>
 </tr>
 </tbody>
 </table>
+<h3 id="kwok.x-k8s.io/v1alpha1.SelectorOperator">
+SelectorOperator
+(<code>string</code> alias)
+<a href="#kwok.x-k8s.io%2fv1alpha1.SelectorOperator"> #</a>
+</h3>
+<p>
+<em>Appears on: </em>
+<a href="#kwok.x-k8s.io/v1alpha1.MatchExpression">MatchExpression</a>
+, 
+<a href="#kwok.x-k8s.io/v1alpha1.SelectorJQ">SelectorJQ</a>
+</p>
+<p>
+<p>SelectorOperator is a label selector operator is the set of operators that can be used in a selector requirement.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><code>&#34;DoesNotExist&#34;</code></td>
+<td><p>SelectorOpDoesNotExist is the negated existence operator.</p>
+</td>
+</tr>
+<tr>
+<td><code>&#34;Exists&#34;</code></td>
+<td><p>SelectorOpExists is the existence operator.</p>
+</td>
+</tr>
+<tr>
+<td><code>&#34;In&#34;</code></td>
+<td><p>SelectorOpIn is the set inclusion operator.</p>
+</td>
+</tr>
+<tr>
+<td><code>&#34;NotIn&#34;</code></td>
+<td><p>SelectorOpNotIn is the negated set inclusion operator.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="kwok.x-k8s.io/v1alpha1.StageDelay">
 StageDelay
 <a href="#kwok.x-k8s.io%2fv1alpha1.StageDelay"> #</a>
@@ -6106,8 +6288,8 @@ If JitterDurationMilliseconds is less than DurationMilliseconds, then JitterDura
 <td>
 <code>durationFrom</code>
 <em>
-<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFromSource">
-ExpressionFromSource
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFrom">
+ExpressionFrom
 </a>
 </em>
 </td>
@@ -6133,8 +6315,8 @@ at random from the interval between DurationMilliseconds and JitterDurationMilli
 <td>
 <code>jitterDurationFrom</code>
 <em>
-<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFromSource">
-ExpressionFromSource
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFrom">
+ExpressionFrom
 </a>
 </em>
 </td>
@@ -6585,13 +6767,13 @@ operator is &ldquo;In&rdquo;, and the values array contains only &ldquo;value&rd
 <td>
 <code>matchExpressions</code>
 <em>
-<a href="#kwok.x-k8s.io/v1alpha1.SelectorRequirement">
-[]SelectorRequirement
+<a href="#kwok.x-k8s.io/v1alpha1.MatchExpression">
+[]MatchExpression
 </a>
 </em>
 </td>
 <td>
-<p>MatchExpressions is a list of label selector requirements. The requirements are ANDed.</p>
+<p>MatchExpressions is a list of label selector expressions. The requirements are ANDed.</p>
 </td>
 </tr>
 </tbody>
@@ -6657,8 +6839,8 @@ a random stage will be matched as the next stage based on the weight.</p>
 <td>
 <code>weightFrom</code>
 <em>
-<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFromSource">
-ExpressionFromSource
+<a href="#kwok.x-k8s.io/v1alpha1.ExpressionFrom">
+ExpressionFrom
 </a>
 </em>
 </td>


Test patch:
diff --git a/pkg/kwok/controllers/stage_controller_test.go b/pkg/kwok/controllers/stage_controller_test.go
index 71ed4fa0..7a037a53 100644
--- a/pkg/kwok/controllers/stage_controller_test.go
+++ b/pkg/kwok/controllers/stage_controller_test.go
@@ -61,11 +61,13 @@ func TestStageController(t *testing.T) {
 			},
 			Spec: internalversion.StageSpec{
 				Selector: &internalversion.StageSelector{
-					MatchExpressions: []internalversion.SelectorRequirement{
+					MatchExpressions: []internalversion.MatchExpression{
 						{
-							Key:      ".status.phase",
-							Operator: "NotIn",
-							Values:   []string{"Available"},
+							JQ: &internalversion.SelectorJQ{
+								Key:      ".status.phase",
+								Operator: "NotIn",
+								Values:   []string{"Available"},
+							},
 						},
 					},
 				},
diff --git a/pkg/utils/expression/value_duration_from_test.go b/pkg/utils/expression/value_duration_from_test.go
deleted file mode 100644
index da641f7a..00000000
--- a/pkg/utils/expression/value_duration_from_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
-Copyright 2022 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 expression
-
-import (
-	"context"
-	"testing"
-	"time"
-
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	"sigs.k8s.io/kwok/pkg/utils/format"
-)
-
-func TestDurationFrom_Get(t *testing.T) {
-	now := time.Now().Truncate(time.Second)
-	nowPlugOneSecond := metav1.NewTime(now.Add(time.Second))
-	type args struct {
-		value *time.Duration
-		src   *string
-		v     interface{}
-	}
-	tests := []struct {
-		name    string
-		args    args
-		wantErr bool
-		want    time.Duration
-		wantOk  bool
-	}{
-		{
-			args: args{
-				value: nil,
-				src:   nil,
-				v:     corev1.Pod{},
-			},
-			wantOk: false,
-		},
-		{
-			args: args{
-				value: format.Ptr(time.Duration(0)),
-				src:   nil,
-				v:     corev1.Pod{},
-			},
-			want:   0,
-			wantOk: true,
-		},
-		{
-			args: args{
-				value: format.Ptr(time.Duration(1)),
-				src:   format.Ptr(".metadata.deletionTimestamp"),
-				v: corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						DeletionTimestamp: &nowPlugOneSecond,
-					},
-				},
-			},
-			want:   1 * time.Second,
-			wantOk: true,
-		},
-		{
-			args: args{
-				src: format.Ptr(".metadata.annotations[\"custom-duration\"]"),
-				v: corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Annotations: map[string]string{"custom-duration": "7s"},
-					},
-				},
-			},
-			want:   7 * time.Second,
-			wantOk: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			d, err := NewDurationFrom(tt.args.value, tt.args.src)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("NewDurationFrom() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			got, gotOk := d.Get(context.Background(), tt.args.v, now)
-			if got != tt.want {
-				t.Errorf("Get() got = %v, want %v", got, tt.want)
-			}
-			if gotOk != tt.wantOk {
-				t.Errorf("Get() gotOk = %v, wantOk %v", gotOk, tt.wantOk)
-			}
-		})
-	}
-}
diff --git a/pkg/utils/expression/value_int_from_test.go b/pkg/utils/expression/value_int_from_test.go
deleted file mode 100644
index 51ae9e22..00000000
--- a/pkg/utils/expression/value_int_from_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
-Copyright 2024 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 expression
-
-import (
-	"context"
-	"testing"
-
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	"sigs.k8s.io/kwok/pkg/utils/format"
-)
-
-func TestNewIntFrom_Get(t *testing.T) {
-	type args struct {
-		value *int64
-		src   *string
-		v     interface{}
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    int64
-		wantOk  bool
-		wantErr bool
-	}{
-		{
-			args: args{
-				value: nil,
-				src:   nil,
-				v:     corev1.Pod{},
-			},
-		},
-		{
-			args: args{
-				value: format.Ptr[int64](0),
-				src:   nil,
-				v:     corev1.Pod{},
-			},
-			wantOk: true,
-		},
-		{
-			args: args{
-				value: format.Ptr[int64](0),
-				src:   format.Ptr(".metadata.deletionGracePeriodSeconds"),
-				v: corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						DeletionGracePeriodSeconds: format.Ptr[int64](1),
-					},
-				},
-			},
-			want:   1,
-			wantOk: true,
-		},
-		{
-			args: args{
-				value: format.Ptr[int64](0),
-				src:   format.Ptr(".metadata.generation"),
-				v: corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Generation: 1,
-					},
-				},
-			},
-			want:   1,
-			wantOk: true,
-		},
-		{
-			args: args{
-				value: format.Ptr[int64](0),
-				src:   format.Ptr(".metadata.annotations.x"),
-				v: corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Annotations: map[string]string{
-							"x": "1",
-						},
-					},
-				},
-			},
-			want:   1,
-			wantOk: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			d, err := NewIntFrom(tt.args.value, tt.args.src)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("NewDurationFrom() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			got, got1 := d.Get(context.Background(), tt.args.v)
-			if got != tt.want {
-				t.Errorf("Get() got = %v, want %v", got, tt.want)
-			}
-			if got1 != tt.wantOk {
-				t.Errorf("Get() gotOk = %v, want %v", got1, tt.wantOk)
-			}
-		})
-	}
-}
diff --git a/pkg/utils/lifecycle/value_duration_from_test.go b/pkg/utils/lifecycle/value_duration_from_test.go
new file mode 100644
index 00000000..53645bac
--- /dev/null
+++ b/pkg/utils/lifecycle/value_duration_from_test.go
@@ -0,0 +1,173 @@
+/*
+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 lifecycle
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	"sigs.k8s.io/kwok/pkg/apis/internalversion"
+	"sigs.k8s.io/kwok/pkg/utils/cel"
+	"sigs.k8s.io/kwok/pkg/utils/format"
+)
+
+func Test_newDurationFrom_Get(t *testing.T) {
+	now := time.Now().Truncate(time.Second)
+	nowPlugOneSecond := metav1.NewTime(now.Add(time.Second))
+	type args struct {
+		value *time.Duration
+		src   *internalversion.ExpressionFrom
+		event *Event
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+		want    time.Duration
+		wantOk  bool
+	}{
+		{
+			args: args{
+				value: nil,
+				src:   nil,
+				event: &Event{
+					Data: &corev1.Pod{},
+				},
+			},
+			wantOk: false,
+		},
+		{
+			args: args{
+				value: format.Ptr(time.Duration(0)),
+				src:   nil,
+				event: &Event{
+					Data: &corev1.Pod{},
+				},
+			},
+			want:   0,
+			wantOk: true,
+		},
+		{
+			args: args{
+				value: format.Ptr(time.Duration(1)),
+				src: &internalversion.ExpressionFrom{
+					JQ: &internalversion.ExpressionJQ{
+						Expression: ".metadata.deletionTimestamp",
+					},
+				},
+				event: &Event{
+					Data: &corev1.Pod{
+						ObjectMeta: metav1.ObjectMeta{
+							DeletionTimestamp: &nowPlugOneSecond,
+						},
+					},
+				},
+			},
+			want:   1 * time.Second,
+			wantOk: true,
+		},
+		{
+			args: args{
+				src: &internalversion.ExpressionFrom{
+					JQ: &internalversion.ExpressionJQ{
+						Expression: ".metadata.annotations[\"custom-duration\"]",
+					},
+				},
+				event: &Event{
+					Data: &corev1.Pod{
+						ObjectMeta: metav1.ObjectMeta{
+							Annotations: map[string]string{"custom-duration": "7s"},
+						},
+					},
+				},
+			},
+			want:   7 * time.Second,
+			wantOk: true,
+		},
+		{
+			args: args{
+				src: &internalversion.ExpressionFrom{
+					CEL: &internalversion.ExpressionCEL{
+						Expression: "duration(self.metadata.annotations[\"custom-duration\"])",
+					},
+				},
+				event: &Event{
+					Data: &corev1.Pod{
+						ObjectMeta: metav1.ObjectMeta{
+							Annotations: map[string]string{"custom-duration": "7s"},
+						},
+					},
+				},
+			},
+			want:   7 * time.Second,
+			wantOk: true,
+		},
+		{
+			args: args{
+				src: &internalversion.ExpressionFrom{
+					CEL: &internalversion.ExpressionCEL{
+						Expression: "self.spec.duration",
+					},
+				},
+				event: &Event{
+					Data: &unstructured.Unstructured{
+						Object: map[string]any{
+							"spec": map[string]any{
+								"duration": "7s",
+							},
+						},
+					},
+				},
+			},
+			want:   7 * time.Second,
+			wantOk: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			env, _ := cel.NewEnvironment(cel.EnvironmentConfig{
+				Types:       cel.DefaultTypes,
+				Conversions: cel.DefaultConversions,
+				Methods:     cel.FuncsToMethods(cel.DefaultFuncs),
+				Funcs:       cel.DefaultFuncs,
+				Vars:        tt.args.event.toCELStandardTypeOnly(),
+			})
+
+			d, err := newDurationFrom(tt.args.value, env, tt.args.src)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("newDurationFrom() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			got, gotOk, err := d.Get(context.Background(), tt.args.event, now)
+			if err != nil {
+				t.Errorf("Get() error = %v", err)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("Get() got = %v, want %v", got, tt.want)
+			}
+			if gotOk != tt.wantOk {
+				t.Errorf("Get() gotOk = %v, wantOk %v", gotOk, tt.wantOk)
+			}
+		})
+	}
+}
diff --git a/pkg/utils/lifecycle/value_int_from_test.go b/pkg/utils/lifecycle/value_int_from_test.go
new file mode 100644
index 00000000..ea4098d6
--- /dev/null
+++ b/pkg/utils/lifecycle/value_int_from_test.go
@@ -0,0 +1,192 @@
+/*
+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 lifecycle
+
+import (
+	"context"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	"sigs.k8s.io/kwok/pkg/apis/internalversion"
+	"sigs.k8s.io/kwok/pkg/utils/cel"
+	"sigs.k8s.io/kwok/pkg/utils/format"
+)
+
+func Test_newInt64From_Get(t *testing.T) {
+	type args struct {
+		value *int64
+		src   *internalversion.ExpressionFrom
+		event *Event
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    int64
+		wantOk  bool
+		wantErr bool
+	}{
+		{
+			args: args{
+				value: nil,
+				src:   nil,
+				event: &Event{
+					Data: &corev1.Pod{},
+				},
+			},
+		},
+		{
+			args: args{
+				value: format.Ptr[int64](0),
+				src:   nil,
+				event: &Event{
+					Data: &corev1.Pod{},
+				},
+			},
+			wantOk: true,
+		},
+		{
+			args: args{
+				value: format.Ptr[int64](0),
+				src: &internalversion.ExpressionFrom{
+					JQ: &internalversion.ExpressionJQ{
+						Expression: ".metadata.deletionGracePeriodSeconds",
+					},
+				},
+				event: &Event{
+					Data: &corev1.Pod{
+						ObjectMeta: metav1.ObjectMeta{
+							DeletionGracePeriodSeconds: format.Ptr[int64](1),
+						},
+					},
+				},
+			},
+			want:   1,
+			wantOk: true,
+		},
+		{
+			args: args{
+				value: format.Ptr[int64](0),
+				src: &internalversion.ExpressionFrom{
+					JQ: &internalversion.ExpressionJQ{
+						Expression: ".metadata.generation",
+					},
+				},
+				event: &Event{
+					Data: &corev1.Pod{
+						ObjectMeta: metav1.ObjectMeta{
+							Generation: 1,
+						},
+					},
+				},
+			},
+			want:   1,
+			wantOk: true,
+		},
+		{
+			args: args{
+				value: format.Ptr[int64](0),
+				src: &internalversion.ExpressionFrom{
+					JQ: &internalversion.ExpressionJQ{
+						Expression: ".metadata.annotations.x",
+					},
+				},
+				event: &Event{
+					Data: &corev1.Pod{
+						ObjectMeta: metav1.ObjectMeta{
+							Annotations: map[string]string{
+								"x": "1",
+							},
+						},
+					},
+				},
+			},
+			want:   1,
+			wantOk: true,
+		},
+		{
+			args: args{
+				value: format.Ptr[int64](0),
+				src: &internalversion.ExpressionFrom{
+					CEL: &internalversion.ExpressionCEL{
+						Expression: "self.spec.terminationGracePeriodSeconds",
+					},
+				},
+				event: &Event{
+					Data: &corev1.Pod{
+						Spec: corev1.PodSpec{
+							TerminationGracePeriodSeconds: format.Ptr[int64](1),
+						},
+					},
+				},
+			},
+			want:   1,
+			wantOk: true,
+		},
+		{
+			args: args{
+				value: format.Ptr[int64](0),
+				src: &internalversion.ExpressionFrom{
+					CEL: &internalversion.ExpressionCEL{
+						Expression: "self.spec.number",
+					},
+				},
+				event: &Event{
+					Data: &unstructured.Unstructured{
+						Object: map[string]any{
+							"spec": map[string]any{
+								"number": 10,
+							},
+						},
+					},
+				},
+			},
+			want:   10,
+			wantOk: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			env, _ := cel.NewEnvironment(cel.EnvironmentConfig{
+				Types:       cel.DefaultTypes,
+				Conversions: cel.DefaultConversions,
+				Methods:     cel.FuncsToMethods(cel.DefaultFuncs),
+				Funcs:       cel.DefaultFuncs,
+				Vars:        tt.args.event.toCELStandardTypeOnly(),
+			})
+
+			d, err := newInt64From(tt.args.value, env, tt.args.src)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("newInt64From() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			got, got1, err := d.Get(context.Background(), tt.args.event)
+			if err != nil {
+				t.Errorf("Get() error = %v", err)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("Get() got = %v, want %v", got, tt.want)
+			}
+			if got1 != tt.wantOk {
+				t.Errorf("Get() gotOk = %v, want %v", got1, tt.wantOk)
+			}
+		})
+	}
+}