Compare commits

..

4 Commits

Author SHA1 Message Date
Lucas Gomide
c666a646f3 refactor: remove redundant platform tool app assignment
`CrewAIPlatformActionTool` already receives `app` through Pydantic during base initialization. Remove the duplicate assignment to retain one owner for field state.
2026-08-05 18:56:59 -03:00
Lucas Gomide
e819cd5f2b feat: add app metadata to platform action tools
Platform policy matching needs an explicit integration slug on every
`CrewAIPlatformActionTool` at runtime. The builder now propagates each API
app key and tests cover the resulting tool metadata.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:44:16 -03:00
Vidit Ostwal
319a20c028 docs: update scaffold AGENTS.md for unified create CLI (#6829)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Check Documentation Broken Links / Check broken links (push) Waiting to run
Vulnerability Scan / Detect changes (push) Waiting to run
Vulnerability Scan / pip-audit (push) Blocked by required conditions
Document crewai create tool/skill/template, lifecycle commands, and
deprecated-but-supported scaffolding aliases in the project template
AGENTS.md. Remove the no-op --skip_provider flag from the flow example.
2026-08-06 01:10:19 +05:30
Vidit Ostwal
8320178697 fix(flow): clarify conversational route/handler name collision errors (#6825)
* fix(flow): clarify route/handler name collision validation errors

When @listen(...) includes the handler's own name, FlowDefinition validation
fails. This change improves the error text and how it surfaces for Python
Flow classes.

What changed
- _self_listen_error in flow_definition.py: two message variants (conversational
  vs default), both include the listen condition
- build_flow_definition in dsl/_utils.py: wraps FlowDefinition ValidationError
  with the Python Flow class name
- Tests for declarative and DSL-built flows; docs follow in a separate commit

When each error surfaces

1. Conversational message — FlowDefinition validation when
   conversational.enabled is true and listen references the handler name.
   Example: @listen("create_video") on def create_video in a conversational flow.
   Surfaces via:
   - FlowDefinition.from_declaration(dict/yaml) → pydantic ValidationError for
     FlowDefinition (Value error, methods.create_video.listen listen condition...)
   - MyFlow.flow_definition() / MyFlow() → ValueError Invalid flow definition
     for MyFlow: ... (wrapped by pydantic as ValidationError for MyFlow on
     instantiation)

2. Default (non-conversational) message — same trigger check when the flow is
   not conversational. Example: @listen("publish") on def publish.
   Surfaces via the same paths as (1).

3. Class-name wrapper — only on the Python DSL path when build_flow_definition
   catches FlowDefinition ValidationError. Prepends Invalid flow definition for
   {ClassName}: to the underlying message from (1) or (2). Does not apply to
   from_declaration without a Flow class.

* docs(flow): explain conversational handler naming vs route labels

Document why @listen route labels must differ from handler method names and
recommend the handle_* naming pattern.

* docs(cli): warn against matching @listen labels to handler names

Add AGENTS.md guidance for crew and flow scaffolding so coding assistants
do not name handlers the same as their @listen route or event labels.

* docs(cli): clarify @listen self-reference fails at validation

Document that matching @listen labels to handler names raises a validation
error at flow instantiation, and that the runtime loop only occurs if
validation is bypassed.
2026-08-06 00:14:55 +05:30
12 changed files with 148 additions and 20 deletions

View File

@@ -376,6 +376,27 @@ def handle_internet_search(self) -> str:
...
```
### Naming handlers
The string in `@listen("…")` is a **router route label** (an event name), not the Python method name. Route labels and method completion events share one trigger namespace, so naming a handler the same as its route causes the handler to re-trigger itself in a loop.
Use a different method name — the docs examples use a `handle_*` prefix:
```python
@listen("create_video")
def handle_create_video(self) -> str:
"""User wants a new video."""
...
```
Do **not** mirror the route label on the method:
```python
@listen("create_video")
def create_video(self) -> str: # rejected at flow instantiation
...
```
…and the router LLM sees:
```

View File

@@ -671,7 +671,7 @@ def handle_create_video(self):
...
```
If validation were bypassed, matching names would also cause the handler to re-trigger itself in a loop at runtime. This applies to all flows; it is especially common in **conversational flows** (`conversational = True`), where `@listen("...")` is a router intent name.
If validation were bypassed, matching names would also cause the handler to re-trigger itself in a loop at runtime. This applies to all flows. It is especially common in **conversational flows** (`conversational = True`), where `@listen("...")` is a router intent name — do not name the handler after the route it serves.
### Structured State
```python

View File

@@ -39,8 +39,8 @@ Pick the simplest action that does the job.
- `state` is the initial shared data shape. Action results do not automatically merge into `state`.
- Read method results with `outputs.method_name` after that method can run.
- `listen` targets a method name or a router-emitted event name.
- Methods must not listen to their own method name.
- Method names and emitted event names share one namespace. Avoid reusing the same string for both unless the user explicitly wants that.
- Methods must not listen to their own method name — including when the `listen` value is a route label that matches the method name (e.g. `listen: create_video` on method `create_video`).
- Method names and emitted event names share one namespace. Do not reuse the same string for a method's `listen` target and its method name.
- Use `router: true` plus `emit` when one method chooses between named branches.
- A router action must return exactly one emitted event string. It must not return JSON, a list, or an explanation.
- Use `start: true` for the single entrypoint.
@@ -107,8 +107,8 @@ Dynamic value rules:
- Do not make `do` a list.
- Do not use CEL `+` to build text in action mappings. Keep the text literal and insert each dynamic value with `${...}`.
- Do not reference `outputs.some_method` before `some_method` can run.
- Do not set a method's `listen` to its own method name.
- Do not use the same string for an emitted event and a method name unless the user asks for it.
- Do not set a method's `listen` to its own method name (including matching route labels such as `listen: create_video` on method `create_video`).
- Do not use the same string for a method's `listen` target and its method name.
- Do not use `emit` without `router: true`.
- Do not rely on crew action-level `inputs` alone to ground agent behavior. Inputs that do not match placeholders are effectively unused by the prompt.
- Do not ask agents to infer missing facts when accuracy matters. Tell them to mark missing dates, amounts, offers, logs, or constraints as unknown.

View File

@@ -17,6 +17,7 @@ from crewai_tools.tools.crewai_platform_tools.misc import (
class CrewAIPlatformActionTool(BaseTool):
app: str = Field(description="The integration slug for this action")
action_name: str = Field(default="", description="The name of the action")
action_schema: dict[str, Any] = Field(
default_factory=dict, description="The schema of the action"
@@ -25,6 +26,7 @@ class CrewAIPlatformActionTool(BaseTool):
def __init__(
self,
description: str,
app: str,
action_name: str,
action_schema: dict[str, Any],
):
@@ -46,6 +48,7 @@ class CrewAIPlatformActionTool(BaseTool):
name=action_name.lower().replace(" ", "_"),
description=description,
args_schema=args_schema,
app=app,
)
self.action_name = action_name
self.action_schema = action_schema

View File

@@ -89,6 +89,7 @@ class CrewaiPlatformToolBuilder:
tool = CrewAIPlatformActionTool(
description=description,
app=function_details["app"],
action_name=action_name,
action_schema=action_schema,
)

View File

@@ -28,6 +28,7 @@ class TestCrewAIPlatformActionToolVerify:
def create_test_tool(self):
return CrewAIPlatformActionTool(
description="Test action tool",
app="test_app",
action_name="test_action",
action_schema=self.action_schema
)

View File

@@ -171,6 +171,10 @@ class TestCrewaiPlatformToolBuilder(unittest.TestCase):
tool_names = [tool.action_name for tool in tools]
assert "create_issue" in tool_names
assert "send_message" in tool_names
assert {tool.action_name: tool.app for tool in tools} == {
"create_issue": "github",
"send_message": "slack",
}
github_tool = next((t for t in tools if t.action_name == "create_issue"), None)
slack_tool = next((t for t in tools if t.action_name == "send_message"), None)

View File

@@ -4,7 +4,7 @@ import json
import logging
from typing import Any, ParamSpec, TypeVar
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
from typing_extensions import TypeIs
from crewai.flow.flow_definition import (
@@ -432,6 +432,20 @@ def _iter_flow_methods(flow_class: type) -> dict[str, Any]:
return methods
def _flow_definition_validation_error(
flow_class: type, exc: ValidationError
) -> ValueError:
errors = exc.errors()
if errors:
detail = errors[0].get("msg", str(exc))
if isinstance(detail, str) and detail.startswith("Value error, "):
detail = detail.removeprefix("Value error, ")
else:
detail = str(exc)
class_name = getattr(flow_class, "__name__", "Flow")
return ValueError(f"Invalid flow definition for {class_name}: {detail}")
def _build_flow_definition_from_class(
flow_class: type,
namespace: dict[str, Any] | None = None,
@@ -455,15 +469,18 @@ def _build_flow_definition_from_class(
if docstring:
description = docstring.strip()
definition = FlowDefinition(
name=getattr(flow_class, "__name__", "Flow"),
description=description,
state=_build_state_definition(flow_class),
config=_build_config_definition(flow_class),
persist=_build_persistence_definition(flow_class),
conversational=_build_conversational_definition(flow_class),
methods=methods,
)
try:
definition = FlowDefinition(
name=getattr(flow_class, "__name__", "Flow"),
description=description,
state=_build_state_definition(flow_class),
config=_build_config_definition(flow_class),
persist=_build_persistence_definition(flow_class),
conversational=_build_conversational_definition(flow_class),
methods=methods,
)
except ValidationError as exc:
raise _flow_definition_validation_error(flow_class, exc) from exc
log_flow_definition_issues(definition)
return definition

View File

@@ -775,7 +775,11 @@ class FlowDefinition(BaseModel):
for method_name, method in self.methods.items():
if _condition_references(method.listen, method_name):
raise ValueError(
f"methods.{method_name}.listen must not reference itself"
_self_listen_error(
method_name=method_name,
listen=method.listen,
definition=self,
)
)
return self
@@ -888,6 +892,39 @@ def _condition_references(condition: FlowDefinitionCondition | None, name: str)
)
def _format_listen_condition(condition: FlowDefinitionCondition | None) -> str:
if condition is None:
return "None"
return repr(condition)
def _self_listen_error(
*,
method_name: str,
listen: FlowDefinitionCondition | None,
definition: FlowDefinition,
) -> str:
path = f"methods.{method_name}.listen"
listen_display = _format_listen_condition(listen)
conversational = (
definition.conversational is not None and definition.conversational.enabled
)
if conversational:
return (
f"{path} listen condition {listen_display} matches the handler name "
f"{method_name!r}. In conversational flows, @listen labels are router "
"route names — they share the same trigger namespace as method completion "
"events, so this handler would re-run in a loop. Rename the handler "
f"(for example, handle_{method_name}) or use a different route label."
)
return (
f"{path} listen condition {listen_display} references the handler name "
f"{method_name!r}. A listener triggered by its own completion creates an "
"infinite loop. Listen to a different method or event, or rename the handler."
)
def _validate_action_cel(
action: FlowActionDefinition,
*,

View File

@@ -2158,7 +2158,7 @@ def test_self_listening_method_is_rejected():
def process(self):
pass
with pytest.raises(ValueError, match="methods.process.listen"):
with pytest.raises(ValueError, match="Invalid flow definition for SelfListenFlow"):
SelfListenFlow.flow_definition()
@@ -2176,7 +2176,7 @@ def test_or_condition_self_listen_is_rejected():
def process(self):
pass
with pytest.raises(ValueError, match="methods.process.listen"):
with pytest.raises(ValueError, match="Invalid flow definition for OrSelfListenFlow"):
OrSelfListenFlow.flow_definition()
@@ -2190,7 +2190,7 @@ def test_router_self_listening_method_is_rejected():
def route(self):
return "done"
with pytest.raises(ValueError, match="methods.route.listen"):
with pytest.raises(ValueError, match="Invalid flow definition for RouterSelfListenFlow"):
RouterSelfListenFlow.flow_definition()

View File

@@ -1231,7 +1231,7 @@ def test_static_string_listener_is_allowed_by_contract():
@pytest.mark.parametrize("listen", ["publish", {"or": ["publish", "revise"]}])
@pytest.mark.parametrize("router_enabled", [False, True])
def test_flow_definition_rejects_method_self_listen(listen, router_enabled):
with pytest.raises(ValueError, match="methods.publish.listen"):
with pytest.raises(ValueError, match="listen condition"):
flow_definition.FlowDefinition.from_declaration(contents=
{
"schema": "crewai.flow/v1",
@@ -1252,6 +1252,49 @@ def test_flow_definition_rejects_method_self_listen(listen, router_enabled):
)
def test_flow_definition_rejects_conversational_route_handler_name_collision():
with pytest.raises(ValueError, match=r"listen condition 'create_video'"):
flow_definition.FlowDefinition.from_declaration(contents=
{
"schema": "crewai.flow/v1",
"name": "VideoFlow",
"conversational": {
"enabled": True,
"router": {
"route_descriptions": {
"create_video": "User wants a new video.",
},
},
},
"methods": {
"begin": {
"do": {"ref": "loaded_flows:VideoFlow.begin"},
"start": True,
},
"create_video": {
"do": {"ref": "loaded_flows:VideoFlow.create_video"},
"listen": "create_video",
},
},
}
)
def test_build_flow_definition_wraps_validation_error_with_class_name():
class VideoFlow(Flow):
conversational = True
@listen("create_video")
def create_video(self):
return "made a video"
with pytest.raises(ValueError, match="Invalid flow definition for VideoFlow"):
VideoFlow.flow_definition()
with pytest.raises(ValueError, match="Invalid flow definition for VideoFlow"):
VideoFlow()
def test_start_false_not_classified_as_start_method():
definition = flow_definition.FlowDefinition.from_declaration(contents=
{

View File

@@ -1698,6 +1698,7 @@ class TestPlatformActionTool:
return mod.CrewAIPlatformActionTool(
description="Send a Slack message",
app="slack",
action_name="slackbot_send_message",
action_schema={
"function": {