This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 6a6a5b33 [docs] Document Action trigger conditions (#977)
6a6a5b33 is described below
commit 6a6a5b336ae46ad042a047ca3c2d1104620f5cd1
Author: Kerui Wang <[email protected]>
AuthorDate: Tue Aug 18 20:47:08 2026 +0800
[docs] Document Action trigger conditions (#977)
Generated-by: Codex CLI 0.144.5 (GPT-5.6-sol)
Co-authored-by: WenjinXie <[email protected]>
---
docs/content/docs/development/workflow_agent.md | 179 ++++++++++++++++++---
docs/content/docs/development/yaml.md | 33 +++-
.../docs/get-started/quickstart/yaml_agent.md | 2 +-
docs/content/docs/operations/configuration.md | 1 +
4 files changed, 186 insertions(+), 29 deletions(-)
diff --git a/docs/content/docs/development/workflow_agent.md
b/docs/content/docs/development/workflow_agent.md
index 10b1c226..b8166cd0 100644
--- a/docs/content/docs/development/workflow_agent.md
+++ b/docs/content/docs/development/workflow_agent.md
@@ -26,7 +26,12 @@ under the License.
A workflow style agent in Flink-Agents is an agent whose reasoning and
behavior are organized as a directed workflow of modular steps, called actions,
connected by events. This design is inspired by the need to orchestrate
complex, multi-stage tasks in a transparent, extensible, and data-centric way,
leveraging Apache Flink's streaming architecture.
-In Flink-Agents, a workflow agent is defined as a class that inherits from the
`Agent` base class. The agent's logic is expressed as a set of actions, each of
which is a function decorated with `@action(EventType.X)` in python (or a
method annotated with `@Action(EventType.X)` in java). Actions consume events,
perform reasoning or tool calls, and emit new events, which may trigger
downstream actions. This event-driven workflow forms a directed cyclic graph of
computation, where each node [...]
+In Flink-Agents, a workflow agent is a class that inherits from the `Agent`
base class. Its workflow
+is defined by actions: functions decorated with `@action(...)` in Python or
methods annotated with
+`@Action(...)` in Java. Each action declares one or more trigger conditions,
which may be exact event
+types or condition expressions. Actions consume events, perform reasoning or
tool calls, and emit new
+events that may trigger other actions. This event-driven workflow forms a
directed graph that may
+contain cycles. Each node is an action, and each edge represents an event flow
between actions.
A workflow agent is well-suited for scenarios where the solution requires
explicit orchestration, branching, or multi-step reasoning, such as data
enrichment, multi-tool pipelines, or complex business logic.
@@ -230,12 +235,14 @@ public class ReviewAnalysisAgent extends Agent {
## Action
-An action is a piece of code that can be executed. Each action listens to at
least one type of event. When an event of the listening type occurs, the action
will be triggered. An action can also generate new events, to trigger other
actions.
+An action is a piece of code that can be executed. It declares one or more
trigger conditions and is
+triggered when an event matches one of them.
-
-To declare an action in Agent, user can use `@action` to decorate a function
of Agent class in python (or annotate a method of Agent class in java), and
declare the listened event types as decorator/annotation parameters.
-
-The decorated/annotated function signature should be `(Event, RunnerContext)
-> None`. In Python, actions can also be defined as `async def` when using
async execution (see [Async Execution](#async-execution)).
+Use `@action(*trigger_conditions, target=None)` to decorate a Python function
or `@Action({...})` to
+annotate a Java method. The function or method accepts the triggering `Event`
and a `RunnerContext`.
+It sends events through the context rather than returning a result, so declare
its return type as
+`None` in Python or `void` in Java. A native Java action must be `public
static`. Python actions can
+also be defined as `async def` when using async execution (see [Async
Execution](#async-execution)).
{{< tabs "Action Function" >}}
@@ -264,9 +271,78 @@ public class ReviewAnalysisAgent extends Agent {
{{< /tabs >}}
-In the function, user can also send new events, to trigger other actions, or
output the data.
+### Registering Actions Programmatically
+
+As an alternative to annotations, register an action through the `Agent` API:
+
+{{< tabs "Programmatic Action" >}}
+
+{{< tab "Python" >}}
+```python
+agent.add_action(
+ "process_event",
+ [EventType.InputEvent],
+ process_event,
+)
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+public class ProgrammaticAgent extends Agent {
+ public ProgrammaticAgent() throws NoSuchMethodException {
+ addAction(
+ new String[] {EventType.InputEvent},
+ ProgrammaticAgent.class.getMethod(
+ "processEvent", Event.class, RunnerContext.class));
+ }
+
+ public static void processEvent(Event event, RunnerContext ctx) {
+ // Handle the input event.
+ }
+}
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+### Trigger Conditions
+
+A trigger condition is either:
+
+- An **exact event type**, such as `EventType.InputEvent` or a custom event's
`EVENT_TYPE` constant.
+- A **condition expression** that evaluates event data to `true` or `false`.
Expressions are written
+ in [Common Expression Language (CEL)](https://cel.dev/); Flink Agents
supports the subset described
+ in [Trigger Condition Reference](#trigger-condition-reference).
+
+Conditions on the same action are alternatives (OR). For example, separate
conditions for
+`EventType.InputEvent` and `attributes.urgent == true` allow any input event
to trigger the action,
+even when `urgent` is false. To require both, combine them in one expression:
+
+```text
+type == EventType.InputEvent && attributes.urgent == true
+```
-**Trigger another action** — send a built-in or custom event that another
action listens to:
+When passed directly to `@action` or `@Action`, `EventType.InputEvent`
declares an exact event-type
+condition. Within an expression, it is a value and must be compared with
`type`, as shown above. A
+bare identifier or dotted path is treated as an exact event type, so write
attribute checks
+explicitly, for example `ready == true` or `attributes.ready == true`.
+
+#### Matching Behavior
+
+When an event arrives:
+
+1. The runtime selects every action with an exact event-type condition that
matches the event's type.
+2. For each remaining action, it evaluates expressions in declaration order
and stops at the first
+ `true` result.
+3. Every selected action is executed once, even if more than one of its
conditions matches.
+
+### Sending Events
+
+An action can send an event to trigger another action or emit output
downstream.
+
+**Trigger another action** — send a built-in or custom event that matches
another action's trigger
+conditions:
{{< tabs "Trigger Another Action" >}}
@@ -275,7 +351,6 @@ In the function, user can also send new events, to trigger
other actions, or out
@action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
- # send a ChatRequestEvent to trigger the built-in chat-model action
ctx.send_event(ChatRequestEvent(model="my_model", messages=messages))
```
{{< /tab >}}
@@ -284,8 +359,6 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
```java
@Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws
Exception {
- InputEvent inputEvent = InputEvent.fromEvent(event);
- // send ChatRequestEvent
ctx.sendEvent(new ChatRequestEvent("my_model", messages));
}
```
@@ -293,16 +366,15 @@ public static void processInput(Event event,
RunnerContext ctx) throws Exception
{{< /tabs >}}
-**Emit downstream output** — send an `OutputEvent` to produce an output of the
agent:
+**Emit downstream output** — send an `OutputEvent`:
{{< tabs "Emit Output" >}}
{{< tab "Python" >}}
```python
-@action(ChatResponseEvent.EVENT_TYPE)
+@action(EventType.ChatResponseEvent)
@staticmethod
def emit_output(event: Event, ctx: RunnerContext) -> None:
- # output data to downstream
ctx.send_event(OutputEvent(output=result))
```
{{< /tab >}}
@@ -311,7 +383,6 @@ def emit_output(event: Event, ctx: RunnerContext) -> None:
```java
@Action(EventType.ChatResponseEvent)
public static void emitOutput(Event event, RunnerContext ctx) {
- // output data to downstream
ctx.sendEvent(new OutputEvent(result));
}
```
@@ -320,14 +391,69 @@ public static void emitOutput(Event event, RunnerContext
ctx) {
{{< /tabs >}}
{{< hint info >}}
-An `OutputEvent` is collected and emitted to the agent's downstream
**immediately**, bypassing
-action routing, while other events (such as `ChatRequestEvent`) are routed to
the actions that
-listen for them. Sending a `ChatRequestEvent` and an `OutputEvent` from the
same action is valid
-API usage, but it produces both an immediate output and, once the chat
response is handled, a
-later model-based output. For the normal chat request/response workflow, emit
the `OutputEvent`
-from the action that handles the `ChatResponseEvent`, as shown above.
+`OutputEvent` is emitted directly downstream and bypasses action matching.
Therefore, an
+`OutputEvent` trigger never invokes an action. Other events go through action
matching as usual.
{{< /hint >}}
+### Trigger Condition Reference
+
+#### Custom Event-Type Names
+
+A custom event type may be a bare name such as `order.created` or
`order-created`. Each dot-separated
+segment must start with an ASCII letter or underscore and may then contain
ASCII letters, digits,
+underscores, or hyphens. Quote a name that contains other punctuation or would
otherwise be parsed as
+an expression, for example `'order:created'`, `'true'`, or
`'EventType.custom'`. In Java and Python,
+the quotes are part of the condition string. For example, use
`@Action("'order:created'")` in Java or
+`@action("'order:created'")` in Python. A quoted name matches the literal
event-type string; it does
+not reference a built-in `EventType` constant. Quoted names must be non-empty
and cannot contain
+whitespace, quotes, backslashes, or control characters.
+
+#### Expression Variables
+
+Java and Python actions use the same Java runtime to evaluate condition
expressions. Expressions can
+access these framework variables:
+
+| Variable | Value |
+|--------------|--------------------------------------|
+| `type` | The event type string |
+| `id` | The event ID as a string |
+| `EventType` | The built-in event-type constants |
+| `attributes` | The event's top-level attribute map |
+
+#### Accessing Attributes
+
+- Top-level attributes are also available as bare variables, so `score > 80`
and
+ `attributes.score > 80` refer to the same field.
+- Nested values are not flattened. For `{input: {status: "ok"}}`, use
`input.status` or
+ `attributes.input.status`; bare `status` does not refer to the nested value.
Other event payloads
+ keep their top-level envelope, for example `response.content`.
+- Framework variables take precedence over attributes with the same names. Use
`attributes["type"]`
+ or `attributes["id"]` to access a colliding attribute.
+- For a top-level key containing dots, use a literal index such as
`attributes["a.b.c"]`. Test its
+ presence with `"a.b.c" in attributes`.
+
+#### Missing Attributes and Value Types
+
+A present attribute whose value is `null` remains `null`, while a missing
attribute remains absent.
+Use `has(attributes.field)` before reading an optional attribute. Reading a
missing attribute without
+a guard causes an event-time evaluation failure. With the default
`WARN_AND_SKIP` strategy, the
+runtime logs a warning, treats that condition as false, and continues with
later OR conditions;
+`FAIL` fails the Flink task. See the
+[condition evaluation failure strategy]({{< ref
"docs/operations/configuration#core-options" >}}).
+
+Strings remain strings even when they contain JSON. To match nested data, send
a structured map or
+list instead of a JSON-encoded string. Decimal values and integers outside the
signed 64-bit range are
+evaluated as doubles and may lose precision.
+
+#### Limitations
+
+- `has(...)` is the only supported CEL macro. The comprehension macros
`exists`, `exists_one`,
+ `all`, `filter`, and `map` are not supported.
+- Dynamic root access such as `attributes[key]` and operations over the whole
`attributes` map are
+ rejected when the runtime compiles the condition. Operator initialization
fails, so the job cannot
+ start. This happens before event-time evaluation and is not controlled by
the condition evaluation
+ failure strategy. Dynamic access inside a selected top-level attribute
remains supported.
+
### Durable Execution
Use durable execution when you wrap a time-consuming or side-effecting
operation. The framework persists the result and replays it on recovery when
the same call is encountered, so the function will not be called again and side
effects are avoided. When recovery re-enters an action that has not been
recorded as completed, code outside `durable_execute` / `durable_execute_async`
will still be re-executed.
@@ -580,12 +706,17 @@ public class MyAgent extends Agent {
## Event
-Events are JSON-serializable messages passed between actions. Every event has
a `type` string used for routing and an `attributes` map that carries the
payload. A single event may trigger multiple actions if they are all listening
to its type.
+Events are JSON-serializable messages passed between actions. Every event has
a `type` string and an
+`attributes` map that carries its payload. Action routing can match the exact
event type or evaluate
+a condition expression against the event data. One event may trigger multiple
actions.
### Special Events
-* `InputEvent`: Generated by the framework, carrying an input data record that
arrives at the agent in its `input` attribute. Actions listening to
`InputEvent` are the entry points of the agent.
-* `OutputEvent`: The framework listens to `OutputEvent` and converts its
`output` attribute into outputs of the agent.
+* `InputEvent`: Generated by the framework when an input record arrives. The
record is available in
+ the event's `input` attribute. Actions whose trigger conditions match an
`InputEvent` are the
+ agent's entry points.
+* `OutputEvent`: When an action sends this event, the framework emits its
`output` attribute
+ downstream as an agent output. It bypasses action matching.
### Unified Event
diff --git a/docs/content/docs/development/yaml.md
b/docs/content/docs/development/yaml.md
index b70653f7..b4837566 100644
--- a/docs/content/docs/development/yaml.md
+++ b/docs/content/docs/development/yaml.md
@@ -278,19 +278,38 @@ Inline action (map) fields:
|-------|----------|-------------|
| `name` | yes | Action name (unique within the agent). |
| `function` | yes | Fully-qualified callable in the form
`<module-or-class>:<qualname>`. See [Function
references](#function-references). |
-| `trigger_conditions` | yes | List of event types the action listens to.
Built-in [event aliases](#event-aliases) (`input`, `chat_request`, ...) or your
own event-type strings. |
+| `trigger_conditions` | yes | Non-empty list of strings containing exact
event types or condition expressions that return a Boolean. Each string must
contain at least one non-whitespace character. Built-in [event
aliases](#event-aliases) (`input`, `chat_request`, ...) may be used as complete
entries. All entries use OR semantics. |
| `type` | no | Implementation language: `python` or `java`. Defaults to
`python` (see [Selecting the implementation
language](#selecting-the-implementation-language)). |
| `config` | no | Free-form configuration map passed to the action at runtime.
|
+For built-in event types, prefer an [event alias](#event-aliases) such as
`input`. Trigger-condition
+matching and expression semantics are shared with annotated and
programmatically registered actions;
+see [Trigger Conditions]({{< ref
"docs/development/workflow_agent#trigger-conditions" >}}).
+
+Every entry must remain a non-empty string after YAML parsing. Quote condition
expressions, for
+example `"ready == true"`. Quotes are required for literal expressions such as
`"true"`; without
+them, YAML parses `true`, `false`, or `null` as a non-string value. When an
exact custom event type
+requires quotes, preserve those quotes inside the YAML string:
+
+```yaml
+trigger_conditions:
+ - order-created
+ - "'order:created'"
+ - "'EventType.custom'"
+```
+
+An `actions` list can mix inline actions and shared-action references:
+
```yaml
actions:
- name: action1
function: my_pkg.actions:action1
- trigger_conditions: [input]
+ trigger_conditions: [input, custom_event]
type: python
- name: action2
function: my_pkg.actions:action2
- trigger_conditions: [chat_response]
+ trigger_conditions:
+ - "type == EventType.ChatResponseEvent && response.content != ''"
type: python
- action3 # shared action reference (declared at file
level)
```
@@ -498,7 +517,7 @@ When `type:` resolves to the **opposite** language of the
loader, the loader bui
### Provider aliases
-For `clazz:` on resource descriptors and for event names in
`trigger_conditions:`, you can use a short alias instead of a fully-qualified
class path.
+For `clazz:` on resource descriptors and for complete event-type entries in
`trigger_conditions:`, you can use a short alias instead of a fully-qualified
class path. Event alias replacement is an exact complete-entry lookup: `input`
is replaced, while `type == input`, `attributes.kind == 'input'`, and the
quoted event type `'input'` remain unchanged.
#### Event aliases
@@ -513,6 +532,12 @@ For `clazz:` on resource descriptors and for event names
in `trigger_conditions:
| `context_retrieval_request` | `ContextRetrievalRequestEvent` |
| `context_retrieval_response` | `ContextRetrievalResponseEvent` |
+{{< hint warning >}}
+`output` resolves to `OutputEvent`, but output events are emitted directly
downstream and bypass
+action matching. Therefore, using `output` as a trigger condition never
invokes an action. See
+[Sending events]({{< ref "docs/development/workflow_agent#sending-events" >}}).
+{{< /hint >}}
+
For custom event types defined in your code, write the event's full
`EVENT_TYPE` string instead of an alias.
#### Class aliases
diff --git a/docs/content/docs/get-started/quickstart/yaml_agent.md
b/docs/content/docs/get-started/quickstart/yaml_agent.md
index 82fb55bc..949995c4 100644
--- a/docs/content/docs/get-started/quickstart/yaml_agent.md
+++ b/docs/content/docs/get-started/quickstart/yaml_agent.md
@@ -188,7 +188,7 @@ agents:
A few things to notice in the YAML above:
- `clazz: ollama` is an alias resolved by the loader to the full Ollama
chat-model class — see the alias table in the [YAML API]({{< ref
"docs/development/yaml#class-aliases" >}}) doc.
-- `trigger_conditions: [input]` / `[chat_response]` use **event aliases** for
the framework's built-in events.
+- `trigger_conditions: [input]` / `[chat_response]` use **event aliases** for
the framework's built-in events. An alias replaces only a complete event-type
entry; it never rewrites text inside a condition expression or quoted event
type.
- `function:` strings use the `<module-or-class>:<qualname>` format. The right
side is the class-qualified method name, so the YAML reuses the same
`process_input` / `processInput` static methods the original
`ReviewAnalysisAgent` already defines.
- The prompt is declared inline as a `messages:` list and referenced from the
chat-model setup by name.
diff --git a/docs/content/docs/operations/configuration.md
b/docs/content/docs/operations/configuration.md
index 2dc17981..c1918bb5 100644
--- a/docs/content/docs/operations/configuration.md
+++ b/docs/content/docs/operations/configuration.md
@@ -127,6 +127,7 @@ Here is the list of all built-in core configuration options.
| `baseLogDir` | (none) | String
| Base directory for file-based event logs. If not set, uses
`java.io.tmpdir/flink-agents`. Setting this value also implicitly switches
`eventLoggerType` to `file`.
|
| `prettyPrint` | false | boolean
| Whether to enable pretty-printed JSON format for event logs. When set to
`true`, each event is written as formatted multi-line JSON instead of JSONL
(JSON Lines) format. {{< hint info >}}Note: enabling this option makes the log
file no longer valid JSONL format. {{< /hint >}} |
| `event-listeners` | none | `List<String>`
| The list of event listener class names. Each class must implement the
EventListener interface and provide a public no-argument constructor. {{< hint
warning >}} Note: Currently, custom event listeners are only supported in Java.
{{< /hint >}} |
+| `action.trigger-condition.evaluate-failure-strategy` | `WARN_AND_SKIP` |
ConditionEvaluationFailureStrategy | Handles event-time failures while
preparing variables for or evaluating a compiled condition, including a dynamic
non-Boolean result. <br/><ul><li>`WARN_AND_SKIP` (default): log a warning,
treat that condition as false, and continue with later OR
conditions.</li><li>`FAIL`: throw `IllegalStateException` and fail the Flink
task; recovery follows the job's restart configuration.< [...]
| `error-handling-strategy` | ErrorHandlingStrategy.FAIL |
ErrorHandlingStrategy | Strategy for handling errors during model requests,
include timeout and unexpected output schema. <br/>The option value could
be:<br/> <ul><li>`ErrorHandlingStrategy.FAIL`</li>
<li>`ErrorHandlingStrategy.RETRY`</li> <li>`ErrorHandlingStrategy.IGNORE`</li> |
| `max-retries` | 3 | int
| Number of retries when using `ErrorHandlingStrategy.RETRY`.
|
| `retry-wait-interval` | 1 | int
| Base wait interval in seconds between retries when using
`ErrorHandlingStrategy.RETRY`. Uses exponential backoff: the actual wait time
for the Nth retry is `retry-wait-interval * 2^(N-1)` seconds. For example, with
default 1s, waits are 1s, 2s, 4s, etc. Retry count and total wait time are
reported in `ChatResponseEvent` and recorded as metrics (`retryCount`,
`retryWaitSec`) under the connection name. |