GitHub user rosemarYuan edited a discussion: [Discussion] Trigger action by Event Field Values
# \[Discussion\] Trigger action by Event Field Values **Search before asking** * [x] I searched in the [issues](https://github.com/apache/flink-agents/issues) and found nothing similar. **Summary** Currently, an `Action` is triggered by matching the incoming event type. It is too coarse-grained when an action should only run for a subset of events based on event fields. This issue extends `@Action` from event-type matching to field-based conditional matching. Users can now declare trigger condition expressions directly on the `Action`. # Motivation In Agent workflows, Action routing often depends on the **content of an event**, not only on its event type. A typical example is `ChatResponseEvent`, which represents the result of an LLM call. The same event type can originate from different upstream Actions and carry different structured outputs, and downstream Actions usually want to handle only a specific subset. For example, a workflow may call the LLM from both a planner Action and a writer Action. Both calls emit `ChatResponseEvent`, but they should be routed differently: planner responses should go to planner logic, while writer responses may be further split into month-plan or day-plan handlers based on a field such as `response.plantype`. ```plaintext PlannerAction -> LLM call #1 -> ChatResponseEvent WriterAction -> LLM call #2 -> ChatResponseEvent response.plantype = "Month" or contains "Day" ``` Today, the framework only sees all of these handlers as listeners of `ChatResponseEvent`. Without condition-based triggers, every handler is invoked for every `ChatResponseEvent`, and each method must repeat its own `if-return` guard logic to check both metadata and payload fields. This splits routing rules between the `@Action` declaration and the method body. It reduces usability, because users must inspect implementation code to understand routing behavior, and reduces observability, because the framework only sees coarse-grained event-type routing. The proposed direction is to make Action routing conditions declarative and framework-visible, so both metadata conditions and content conditions can be expressed directly in the `@Action` declaration while the method body stays focused on business logic: ```java // Metadata only: fires on the planner's response. @Action("type == EventType.ChatResponseEvent && request_sender == 'PlannerAction'") public static void handlePlannerResponse(Event event, RunnerContext ctx) { // ... planner-response handling } // Metadata + content: fires only on writer-generated month plans. @Action("type == EventType.ChatResponseEvent && request_sender == 'WriterAction' && response.plantype == 'Month'") public static void handleWriterMonthPlan(Event event, RunnerContext ctx) { // ... month-plan handling } // Metadata + content: fires on writer responses whose plantype contains "Day". @Action("type == EventType.ChatResponseEvent && request_sender == 'WriterAction' && response.plantype.contains('Day')") public static void handleWriterDayPlan(Event event, RunnerContext ctx) { // ... day-plan handling } ``` > Note: `request_sender` and `response.plantype` are illustrative fields. > `request_sender` is not currently carried by `ChatResponseEvent`; propagating > it from request to response can be introduced as an orthogonal enhancement. # Goals This change aims to make `@Action` triggers more expressive while keeping the existing model simple and compatible. Specifically, it supports two capabilities: 1. Provide a user-friendly trigger expression model. 2. Support triggering actions with both simple type matching and field-based conditions. Rollout: a stack of independently reviewable PRs; breakdown tracked in the follow-up umbrella issue. ## Expression Language Choice This design uses CEL as the trigger condition language. Several alternatives were considered. | Option | Pros | Cons | | --- | --- | --- | | Custom parser | Full control over syntax. | High maintenance cost; Java/Python consistency is hard. | | Java/Python callback | Very flexible. | Not declarative or easily serializable; hard to validate, trace, cache, and optimize. | | Annotation predicates | Structured and easy to validate. | Verbose and poor at compound boolean logic. | | CEL expression | Compact, declarative, language-neutral, and runtime-friendly. | Needs a clearly documented supported subset. | CEL is selected because trigger conditions should be declarative, side-effect-free, serializable, and centrally evaluable by the framework. It also provides a better fit for Java/Python consistency than executable callback predicates. # API The `Action` annotation declares a list of one or more **triggers**, each of which is either an **event type name** (e.g. `EventType.InputEvent`, shorthand for `type == <that event type>`) **or** a **CEL boolean expression** (e.g. `"type == EventType.ChatResponseEvent"`). In Java, multiple triggers must be enclosed in`{}`, a single trigger does not need braces. Python passes triggers as ordinary positional arguments. ### Example **Java** ```java // Direct event-type triggers. Entries are OR'ed. @Action({EventType.InputEvent, EventType.OutputEvent}) // Metadata filter. @Action("type == EventType.ChatResponseEvent && request_sender == 'PlannerAction'") // Mixed triggers. Entries are OR'ed. @Action({ EventType.InputEvent, // direct type "type == EventType.ChatResponseEvent && retryCount > 0", // metadata condition "type == EventType.ToolResponseEvent && response.success == false", // payload condition "type == EventType.ChatResponseEvent && response.plantype.contains('Day')" // metadata + payload }) ``` **Python** ```python # Direct event-type triggers. Entries are OR'ed. @action(EventType.InputEvent, EventType.OutputEvent) # Metadata filter. @action("type == EventType.ChatResponseEvent && request_sender == 'PlannerAction'") # Mixed triggers. Entries are OR'ed. @action( EventType.InputEvent, # direct type "type == EventType.ChatResponseEvent && retryCount > 0", # metadata "type == EventType.ToolResponseEvent && response.success == false", # payload "type == EventType.ChatResponseEvent && response.plantype.contains('Day')" # metadata + payload ) ``` ### Notes * Multiple triggers combine with **OR** — the action fires when any one of them matches. * Inside an expression, reference event fields by their **bare names** — the framework automatically rewrites them under `attributes` (e.g. `retryCount` → `attributes.retryCount`, `response.structured_output` → `attributes.response.structured_output`). * The root identifiers `id` and `type` are **reserved** for the framework-owned event id and event type. If a business field would otherwise be named `id` or `type`, rename it (e.g. `orderId`, `requestId`). * `EventType.XxxEvent` is a compile-time shortcut for the canonical event-type string of a **built-in** event (e.g. `EventType.InputEvent`). For non-built-in events, write the fully-qualified event-type string directly in the trigger expression (e.g. `"type == 'myEvent'"`). ### Supported expression Syntax The trigger condition uses a constrained CEL-style expression subset. The same expression syntax can be used for event metadata and payload fields, subject to CEL type-checking rules: | Form | Examples | | --- | --- | | Comparison | `==`, `!=`, `<`, `<=`, `>`, `>=` | | Boolean composition | `&&`, `\|\|`, `!` | | Membership | `status in ['FAILED', 'TIMEOUT']` | | Membership Field presence | `has(TypeId)` | | Nested field access | `response.structured_output` | | String functions | `name.contains('foo')`,`name.startsWith('order_')`,`name.matches('^o_\d+$')` | | Explicit attribute access | `attributes['retryCount'] > 0` | This issue does not propose exposing the full CEL language surface in this version. Phase 1 scopes a small subset for common event-field routing: only `has()` is enabled, while comprehension macros such as `exists`, `exists_one`, `all`, `filter`, and `map` are disabled to keep evaluation simple and predictable. Trigger expressions are checked in stages: syntax errors are caught in AgentPlan; field names and value types are not checked statically — an unknown field or a type mismatch only surfaces when an event arrives. The supported operators, built-ins, macros, and known edge cases are documented in the appendix, full operator semantics follow the [CEL language definition](https://github.com/google/cel-spec/blob/master/doc/langdef.md). # Appendix CEL Usage Rules ### 1 Comparison Operators Comparisons require type compatibility. Cross-type conversion should be explicit where necessary, except that CEL supports comparisons among numeric types such as `int`, `uint`, and `double`. * `A == A -> bool`, `A != A -> bool`: equality and inequality. * `<`, `<=`, `>`, `>=`: support `bool`, `int`, `uint`, `double`, `string`, and `bytes`. * `<` and `<=` also support `google.protobuf.Timestamp` and `google.protobuf.Duration`. ### 2 Logical Operators * `bool && bool -> bool`: logical AND. Errors and unknown values do not necessarily halt evaluation immediately. * `bool || bool -> bool`: logical OR. Errors and unknown values do not necessarily halt evaluation immediately. ### 3 `**in**` Operator List membership: ```text A in list(A) -> bool ``` The time complexity is proportional to list length. Examples: ```text 2 in [1, 2, 3] -> true "a" in ["b", "c"] -> false ``` Map key presence: ```text A in map(A, B) -> bool ``` Expected complexity is `O(1)`. Example: ```text 'key1' in {'key1': 'v1'} -> true ``` ### 4 `**has(...)**` Macro ```text has(message.field) -> bool ``` `has(...)` checks whether a field exists. It supports proto2, proto3, and map key access, with map access supported through select notation. For `attributes` as a map: ```text has(attributes.score) ``` is equivalent to: ```text 'score' in attributes ``` The latter is also a standard CEL style and may be preferred for map key checks. ### 5 Advanced Macros Deferred The following advanced macros are not included in the initial scope: ```text exists(...) exists_one(...) all(...) map(...) filter(...) ``` They can be gradually enabled after the safety model and Java/Python consistency are validated. GitHub link: https://github.com/apache/flink-agents/discussions/726 ---- This is an automatically sent email for [email protected]. To unsubscribe, please send an email to: [email protected]
