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..2fb948cea 100644
--- a/publish/BUILD.bazel
+++ b/publish/BUILD.bazel
@@ -29,6 +29,10 @@ 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:async_options",
"//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..e1acc4261 100644
--- a/runtime/BUILD.bazel
+++ b/runtime/BUILD.bazel
@@ -9,6 +9,10 @@ package(
java_library(
name = "runtime",
exports = [
+ ":async_call",
+ ":async_drain_strategy",
+ ":async_observer",
+ ":async_options",
":descriptor_message_provider",
":evaluation_exception",
":function_overload",
@@ -340,6 +344,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 +388,43 @@ 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"],
+)
+
+java_library(
+ name = "async_options",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_options"],
+)
+
+cel_android_library(
+ name = "async_options_android",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_options_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..9518e1601 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",
@@ -817,6 +821,7 @@ java_library(
tags = [
],
deps = [
+ ":async_options",
":descriptor_type_resolver",
":dispatcher",
":evaluation_exception",
@@ -867,6 +872,7 @@ java_library(
tags = [
],
deps = [
+ ":async_options",
":descriptor_message_provider",
":descriptor_type_resolver",
":dispatcher",
@@ -922,6 +928,7 @@ java_library(
],
deps = [
":activation",
+ ":async_options",
":evaluation_exception",
":evaluation_listener",
":function_binding",
@@ -946,6 +953,7 @@ java_library(
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
+ "@maven//:org_jspecify_jspecify",
],
)
@@ -1277,6 +1285,118 @@ 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 = "async_options",
+ srcs = ["CelAsyncEvaluationOptions.java"],
+ tags = [
+ ],
+ deps = [
+ ":async_drain_strategy",
+ ":async_observer",
+ "//:auto_value",
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
+cel_android_library(
+ name = "async_options_android",
+ srcs = ["CelAsyncEvaluationOptions.java"],
+ tags = [
+ ],
+ deps = [
+ ":async_drain_strategy_android",
+ ":async_observer_android",
+ "//:auto_value",
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
java_library(
name = "program",
srcs = ["Program.java"],
@@ -1288,6 +1408,7 @@ java_library(
":partial_vars",
":variable_resolver",
"@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
],
)
@@ -1302,6 +1423,7 @@ cel_android_library(
":partial_vars_android",
":variable_resolver",
"@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven_android//:com_google_guava_guava",
],
)
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/CelAsyncEvaluationOptions.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java
new file mode 100644
index 000000000..19609e923
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java
@@ -0,0 +1,141 @@
+// 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.auto.value.AutoValue;
+import javax.annotation.concurrent.ThreadSafe;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Options for configuring asynchronous CEL evaluation. */
+@AutoValue
+@ThreadSafe
+public abstract class CelAsyncEvaluationOptions {
+
+ private static final int DEFAULT_MAX_CONCURRENCY = 100;
+ private static final int DEFAULT_MAX_ITERATIONS = 1_000;
+
+ /**
+ * Maximum number of concurrent async function calls in-flight simultaneously. A value <= 0
+ * indicates unbounded concurrency.
+ */
+ public abstract int maxConcurrency();
+
+ /** Strategy governing when to trigger re-evaluation after async call completions. */
+ public abstract CelAsyncDrainStrategy drainStrategy();
+
+ /** Safety cap on the maximum number of AST re-evaluation passes before aborting. */
+ public abstract int maxIterations();
+
+ /**
+ * Returns the custom configured {@link ScheduledExecutorService}, if present.
+ *
+ *
If absent, {@link #resolveScheduledExecutorService()} falls back to an internal, shared
+ * single-threaded daemon scheduler.
+ */
+ public abstract Optional scheduledExecutorService();
+
+ /** Returns the configured lifecycle observer, if present. */
+ public abstract Optional observer();
+
+ /**
+ * Resolves the {@link ScheduledExecutorService} used for debounce timers, falling back to a
+ * shared, lazily initialized single-threaded daemon scheduler (named {@code
+ * cel-async-debounce-*}) if not custom-configured.
+ *
+ *
The scheduler is used exclusively as an alarm clock to trigger continuation wakeups; it does
+ * not execute CEL evaluation tasks.
+ */
+ public ScheduledExecutorService resolveScheduledExecutorService() {
+ return scheduledExecutorService().orElse(DefaultDebounceSchedulerHolder.INSTANCE);
+ }
+
+ public abstract Builder toBuilder();
+
+ /**
+ * Returns a new {@link Builder} initialized with standard default options:
+ *
+ *
Scheduled executor service: A shared, lazily initialized single-threaded daemon scheduler
+ * used exclusively for debounce timer wakeups.
+ *
+ */
+ public static Builder newBuilder() {
+ return new AutoValue_CelAsyncEvaluationOptions.Builder()
+ .setMaxConcurrency(DEFAULT_MAX_CONCURRENCY)
+ .setDrainStrategy(CelAsyncDrainStrategy.drainReady())
+ .setMaxIterations(DEFAULT_MAX_ITERATIONS);
+ }
+
+ /**
+ * Returns a new {@link Builder} initialized with standard default options.
+ *
+ *
Equivalent to calling {@link #newBuilder()}.
+ */
+ public static Builder builder() {
+ return newBuilder();
+ }
+
+ /**
+ * Returns a {@link CelAsyncEvaluationOptions} instance with the {@link #newBuilder() default
+ * configuration}.
+ */
+ public static CelAsyncEvaluationOptions defaultOptions() {
+ return newBuilder().build();
+ }
+
+ private static final class DefaultDebounceSchedulerHolder {
+ private static final AtomicLong counter = new AtomicLong();
+ private static final ScheduledExecutorService INSTANCE =
+ Executors.newSingleThreadScheduledExecutor(
+ r -> {
+ Thread t = new Thread(r);
+ t.setName("cel-async-debounce-" + counter.getAndIncrement());
+ t.setDaemon(true);
+ return t;
+ });
+ }
+
+ /** Builder for {@link CelAsyncEvaluationOptions}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setMaxConcurrency(int maxConcurrency);
+
+ public abstract Builder setDrainStrategy(CelAsyncDrainStrategy drainStrategy);
+
+ public abstract Builder setMaxIterations(int maxIterations);
+
+ /**
+ * Sets a custom {@link ScheduledExecutorService} for debounce timers.
+ *
+ *
If not set, defaults to an internal, shared single-threaded daemon scheduler.
+ */
+ public abstract Builder setScheduledExecutorService(
+ ScheduledExecutorService scheduledExecutorService);
+
+ public abstract Builder setObserver(CelAsyncObserver observer);
+
+ public abstract CelAsyncEvaluationOptions build();
+ }
+
+ // Package-private constructor prevents extension outside package while allowing AutoValue.
+ CelAsyncEvaluationOptions() {}
+}
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