diff --git a/.bazelrc b/.bazelrc
index f6e2f39c0..34a59ec39 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -16,7 +16,7 @@ build --java_language_version=11
common --javacopt=-Xlint:-options
# Remove flag once https://github.com/google/cel-spec/issues/508 and rules_jvm_external is fixed.
-common --incompatible_autoload_externally=proto_library,cc_proto_library,java_proto_library,java_test
+common --incompatible_autoload_externally=proto_library,cc_proto_library,java_proto_library,java_test,java_import
# Limit repository cache size by not caching extracted repository contents
build --repo_contents_cache=
diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel
index 7fd15a769..17089eb82 100644
--- a/publish/BUILD.bazel
+++ b/publish/BUILD.bazel
@@ -29,6 +29,9 @@ COMMON_TARGETS = [
# keep sorted
RUNTIME_TARGETS = [
"//runtime/src/main/java/dev/cel/runtime",
+ "//runtime/src/main/java/dev/cel/runtime:async_call",
+ "//runtime/src/main/java/dev/cel/runtime:async_drain_strategy",
+ "//runtime/src/main/java/dev/cel/runtime:async_observer",
"//runtime/src/main/java/dev/cel/runtime:base",
"//runtime/src/main/java/dev/cel/runtime:interpreter",
"//runtime/src/main/java/dev/cel/runtime:late_function_binding",
diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel
index c87fadca9..fbdbb1107 100644
--- a/runtime/BUILD.bazel
+++ b/runtime/BUILD.bazel
@@ -9,6 +9,9 @@ package(
java_library(
name = "runtime",
exports = [
+ ":async_call",
+ ":async_drain_strategy",
+ ":async_observer",
":descriptor_message_provider",
":evaluation_exception",
":function_overload",
@@ -340,6 +343,11 @@ java_library(
exports = ["//runtime/src/main/java/dev/cel/runtime:function_overload"],
)
+cel_android_library(
+ name = "function_overload_android",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:function_overload_android"],
+)
+
java_library(
name = "descriptor_message_provider",
visibility = ["//:internal"],
@@ -379,3 +387,33 @@ cel_android_library(
name = "partial_vars_android",
exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars_android"],
)
+
+java_library(
+ name = "async_call",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_call"],
+)
+
+cel_android_library(
+ name = "async_call_android",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_call_android"],
+)
+
+java_library(
+ name = "async_drain_strategy",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_drain_strategy"],
+)
+
+cel_android_library(
+ name = "async_drain_strategy_android",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_drain_strategy_android"],
+)
+
+java_library(
+ name = "async_observer",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_observer"],
+)
+
+cel_android_library(
+ name = "async_observer_android",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_observer_android"],
+)
diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel
index 489bb64d8..145341889 100644
--- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel
+++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel
@@ -784,6 +784,7 @@ cel_android_library(
java_library(
name = "function_overload",
srcs = [
+ "CelAsyncFunctionOverload.java",
"CelFunctionOverload.java",
"OptimizedFunctionOverload.java",
],
@@ -800,9 +801,12 @@ java_library(
cel_android_library(
name = "function_overload_android",
srcs = [
+ "CelAsyncFunctionOverload.java",
"CelFunctionOverload.java",
"OptimizedFunctionOverload.java",
],
+ tags = [
+ ],
deps = [
":evaluation_exception",
":unknown_attributes_android",
@@ -1277,6 +1281,88 @@ cel_android_library(
],
)
+java_library(
+ name = "async_call",
+ srcs = ["CelAsyncCall.java"],
+ tags = [
+ ],
+ deps = [
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+cel_android_library(
+ name = "async_call_android",
+ srcs = ["CelAsyncCall.java"],
+ tags = [
+ ],
+ deps = [
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+java_library(
+ name = "async_drain_strategy",
+ srcs = [
+ "CelAsyncDrainAction.java",
+ "CelAsyncDrainStrategy.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":async_call",
+ "//:auto_value",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+cel_android_library(
+ name = "async_drain_strategy_android",
+ srcs = [
+ "CelAsyncDrainAction.java",
+ "CelAsyncDrainStrategy.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":async_call_android",
+ "//:auto_value",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
+java_library(
+ name = "async_observer",
+ srcs = ["CelAsyncObserver.java"],
+ tags = [
+ ],
+ deps = [
+ ":async_call",
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
+cel_android_library(
+ name = "async_observer_android",
+ srcs = ["CelAsyncObserver.java"],
+ tags = [
+ ],
+ deps = [
+ ":async_call_android",
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ "@maven_android//:com_google_guava_guava",
+ ],
+)
+
java_library(
name = "program",
srcs = ["Program.java"],
diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncCall.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncCall.java
new file mode 100644
index 000000000..1582f12a1
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncCall.java
@@ -0,0 +1,34 @@
+// Copyright 2026 Google LLC
+//
+// 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
+//
+// https://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 dev.cel.runtime;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Describes a pending or completed asynchronous function call. */
+@ThreadSafe
+public interface CelAsyncCall {
+
+ /** Returns the unique incremental tracking ID assigned to this call. */
+ long callId();
+
+ /** Returns the AST expression node ID where the call is located. */
+ long exprId();
+
+ /** Returns the name of the function being invoked. */
+ String functionName();
+
+ /** Returns the specific overload ID being invoked. */
+ String overloadId();
+}
diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainAction.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainAction.java
new file mode 100644
index 000000000..845db25a3
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainAction.java
@@ -0,0 +1,57 @@
+// Copyright 2026 Google LLC
+//
+// 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
+//
+// https://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 dev.cel.runtime;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.Immutable;
+import java.time.Duration;
+
+/** Dictates what asynchronous evaluation should do after inspecting completions. */
+@AutoValue
+@Immutable
+public abstract class CelAsyncDrainAction {
+
+ CelAsyncDrainAction() {}
+
+ /** Indicates that the AST should be re-evaluated immediately. */
+ public abstract boolean shouldReevaluate();
+
+ /**
+ * Indicates how long the evaluator should wait for additional completions before deciding to
+ * re-evaluate. A duration of ZERO with reevaluate=false means wait indefinitely for the next
+ * completion.
+ */
+ public abstract Duration waitDuration();
+
+ public static CelAsyncDrainAction waitDuration(Duration duration) {
+ checkNotNull(duration);
+ checkArgument(!duration.isNegative(), "duration must not be negative");
+ if (duration.isZero()) {
+ return reevaluate();
+ }
+ return new AutoValue_CelAsyncDrainAction(false, duration);
+ }
+
+ public static CelAsyncDrainAction reevaluate() {
+ return new AutoValue_CelAsyncDrainAction(true, Duration.ZERO);
+ }
+
+ public static CelAsyncDrainAction waitForMore() {
+ return new AutoValue_CelAsyncDrainAction(false, Duration.ZERO);
+ }
+}
diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainStrategy.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainStrategy.java
new file mode 100644
index 000000000..6d8349f8e
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainStrategy.java
@@ -0,0 +1,101 @@
+// Copyright 2026 Google LLC
+//
+// 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
+//
+// https://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 dev.cel.runtime;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.errorprone.annotations.Immutable;
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * Controls when asynchronous evaluation re-evaluates the AST after async completions.
+ *
+ *
The evaluator consults the strategy each time completions are received.
+ */
+@Immutable
+public interface CelAsyncDrainStrategy {
+
+ /**
+ * Evaluates the current state of asynchronous evaluation and determines the next step.
+ *
+ * @param completedBatch The batch of async call completions accumulated so far in this drain
+ * cycle.
+ * @param activeCallsCount The number of async calls currently launched but unresolved.
+ */
+ CelAsyncDrainAction nextAction(List completedBatch, int activeCallsCount);
+
+ /**
+ * Re-evaluates after a debounce window after the first completion, batching completions that
+ * complete at roughly the same time.
+ */
+ static CelAsyncDrainStrategy drainReady(Duration debounce) {
+ return new DrainReadyStrategy(debounce);
+ }
+
+ /** Re-evaluates with the default debounce window of 100 microseconds. */
+ static CelAsyncDrainStrategy drainReady() {
+ return drainReady(Duration.ofNanos(100_000));
+ }
+
+ /** Re-evaluates immediately as soon as any single call completes. */
+ static CelAsyncDrainStrategy drainNone() {
+ return (completed, active) -> {
+ checkNotNull(completed, "completedBatch must not be null");
+ checkArgument(active >= 0, "activeCallsCount must be non-negative: %s", active);
+ return active == 0 || !completed.isEmpty()
+ ? CelAsyncDrainAction.reevaluate()
+ : CelAsyncDrainAction.waitForMore();
+ };
+ }
+
+ /** Waits for all currently pending calls to finish before re-evaluating. */
+ static CelAsyncDrainStrategy drainAll() {
+ return (completed, active) -> {
+ checkNotNull(completed, "completedBatch must not be null");
+ checkArgument(active >= 0, "activeCallsCount must be non-negative: %s", active);
+ return active == 0 ? CelAsyncDrainAction.reevaluate() : CelAsyncDrainAction.waitForMore();
+ };
+ }
+
+ /** Internal implementation of the drain ready strategy with configurable debounce duration. */
+ @Immutable
+ final class DrainReadyStrategy implements CelAsyncDrainStrategy {
+ private final Duration debounce;
+
+ DrainReadyStrategy(Duration debounce) {
+ this.debounce = checkNotNull(debounce);
+ checkArgument(!debounce.isNegative(), "debounce duration must not be negative");
+ }
+
+ @Override
+ public CelAsyncDrainAction nextAction(List completedBatch, int activeCallsCount) {
+ checkNotNull(completedBatch, "completedBatch must not be null");
+ checkArgument(
+ activeCallsCount >= 0, "activeCallsCount must be non-negative: %s", activeCallsCount);
+ if (activeCallsCount == 0) {
+ return CelAsyncDrainAction.reevaluate();
+ }
+ if (completedBatch.isEmpty()) {
+ return CelAsyncDrainAction.waitForMore();
+ }
+ if (debounce.isZero()) {
+ return CelAsyncDrainAction.reevaluate();
+ }
+ return CelAsyncDrainAction.waitDuration(debounce);
+ }
+ }
+}
diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncFunctionOverload.java
new file mode 100644
index 000000000..685c81573
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncFunctionOverload.java
@@ -0,0 +1,57 @@
+// Copyright 2026 Google LLC
+//
+// 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
+//
+// https://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 dev.cel.runtime;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.errorprone.annotations.Immutable;
+
+/** Represents a CEL custom function overload that executes asynchronously. */
+@Immutable
+public interface CelAsyncFunctionOverload extends CelFunctionOverload {
+
+ /** Invokes the overload asynchronously with evaluated arguments. */
+ ListenableFuture