From 9d72e269e4de6997803e277f1f308ea7223654fa Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Mon, 13 Jul 2026 14:29:36 -0400 Subject: [PATCH] Remove redundant CEL text helper (#6528) This commit removes the redundant CEL text helper, in favor of the easier interpolation syntax. --- .../templates/declarative_flow/AGENTS.md | 14 ++---- lib/cli/tests/test_create_flow.py | 4 +- lib/crewai/src/crewai/flow/expressions.py | 50 ++----------------- lib/crewai/src/crewai/flow/skill.py | 14 ++++-- .../templates/flow_definition_skill.md.j2 | 2 + lib/crewai/tests/test_flow_definition.py | 10 +++- lib/crewai/tests/test_flow_from_definition.py | 42 +++------------- 7 files changed, 38 insertions(+), 98 deletions(-) diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md index 9f96d5efa..727146c52 100644 --- a/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md @@ -59,9 +59,9 @@ Use these expression forms correctly: - Raw CEL: use in `expr`. Do not wrap raw CEL in `${...}`. - Use `${...}` inside action mapping strings to read Flow data with CEL. Example value: `Ticket: ${state.ticket_id}`. - Use `state` for input data. Use `outputs.step_name` for a completed method result. +- In action mapping strings, keep literal text outside `${...}` and interpolate each Flow value directly. Write `Ticket: ${state.ticket_id}`; do not assemble the string with CEL `+`. - If a value is only one `${...}` expression, the result keeps its type. Use this for numbers, booleans, objects, and lists. - If the string has other text, the final value is text. Non-text values become JSON. `null` becomes empty text. -- Use `text(root, "path", "default")` for values that may be missing or null. The default is optional and is `""`. Expression examples: @@ -78,12 +78,6 @@ domains: "${state.domains}" limit: "${state.limit}" ``` -Use a default for missing text: - -```yaml -input: "Ticket ${text(state, \"ticket.id\", \"unknown\")}" -``` - - Crew text: use `{name}` placeholders from crew inputs. Example: `Research {topic}`. - Crew inputs become prompt text only when agent or task text references matching `{name}` placeholders. - Passing an input that is not referenced by any `{name}` placeholder does not ground the crew. If the crew needs a field, put that placeholder in an agent `goal`, task `description`, or task `expected_output`. @@ -111,6 +105,7 @@ Dynamic value rules: - Do not use fields outside the declaration schema. - Do not put more than one action under a method's `do`. - 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. @@ -246,7 +241,7 @@ Shape: Fields: - `call` (required): must be `crew`. Action discriminator. Use crew to run an inline Crew definition. Example: `crew` - `with` (required): inline crew definition. Inline Crew definition to load and execute for this action. Example: `{"agents": {"researcher": {"backstory": "Knows the domain.", "goal": "Research {topic}", "role": "Researcher"}}, "name": "inline_research", "tasks": [{"agent": "researcher", "description": "Research {topic}", "expected_output": "Findings about {topic}", "name": "research_task"}]}` -- `inputs` (optional): map of string to expression data | null; default `null`. Actual kickoff inputs passed to the Crew. Use `${...}` inside action mapping strings to read Flow data with CEL. Example value: `Ticket: ${state.ticket_id}`. Use `state` for input data. Use `outputs.step_name` for a completed method result. If a value is only one `${...}` expression, the result keeps its type. Use this for numbers, booleans, objects, and lists. If the string has other text, the final value is text. Non-text values become JSON. `null` becomes empty text. Use `text(root, "path", "default")` for values that may be missing or null. The default is optional and is `""`. The evaluated values are available to crew agent and task interpolation as `{name}` placeholders; reference each input the crew needs in agent or task text. Example: `{"topic": "${state.topic}"}` +- `inputs` (optional): map of string to expression data | null; default `null`. Runtime inputs passed to the Crew. Insert Flow values with `${...}` and reference each input as `{name}` in agent or task text. Example: `{"topic": "${state.topic}"}` #### Crew Definition (`methods..do[call=crew].with`) @@ -311,7 +306,7 @@ Fields: - `tools` (optional): list[string | map of string to any] | null; default `null`. Tool refs or serialized tool definitions available to this agent. String refs can use CrewAI tool names, `custom:`, or fully qualified `module:Class` references. Example: `["crewai_tools:SerperDevTool", "custom:file_read"]` - `apps` (optional): list[string] | null; default `null`. Platform apps available to this agent. Can contain app names such as `gmail` or app/action refs such as `gmail/send_email`. Example: `["gmail", "slack/send_message"]` - `mcps` (optional): list[string | map of string to any] | null; default `null`. MCP server refs or serialized MCP server configs available to this agent. String refs can use HTTPS URLs, connected MCP integration slugs, or refs with a `#tool_name` suffix for specific tools. Example: `["https://api.weather.com/mcp#get_current_weather", "snowflake", "stripe#list_invoices", {"cache_tools_list": true, "headers": {"Authorization": "Bearer your_token"}, "streamable": true, "url": "https://api.example.com/mcp"}]` -- `input` (required): string. Input passed to the individual agent kickoff outside of a crew. Use one string. Use `${...}` inside action mapping strings to read Flow data with CEL. Example value: `Ticket: ${state.ticket_id}`. Use `state` for input data. Use `outputs.step_name` for a completed method result. If a value is only one `${...}` expression, the result keeps its type. Use this for numbers, booleans, objects, and lists. If the string has other text, the final value is text. Non-text values become JSON. `null` becomes empty text. Use `text(root, "path", "default")` for values that may be missing or null. The default is optional and is `""`. When an agent needs multiple fields, write one string with labels and separators, for example `Ticket ID: ${state.ticket_id}; Message: ${state.message}`. Example: `${state.ticket.body}` +- `input` (required): string. Agent prompt template. Insert Flow values with `${...}`, for example `Ticket: ${state.ticket_id}`. Example: `${state.ticket.body}` #### LLM Definition @@ -349,4 +344,3 @@ Fields: - Crew action-level `inputs` are the Crew kickoff inputs; use CEL-wrapped strings there for runtime values. - Crew agent/task interpolation uses `{name}` placeholders from evaluated crew inputs. - Agent `with.input` must be text. Use `${outputs.method_name.raw}` or a text field like `${outputs.method_name.json_dict.summary}`. - diff --git a/lib/cli/tests/test_create_flow.py b/lib/cli/tests/test_create_flow.py index 8e297a4ad..2e3787dc4 100644 --- a/lib/cli/tests/test_create_flow.py +++ b/lib/cli/tests/test_create_flow.py @@ -29,7 +29,9 @@ def test_create_flow_declarative_project_can_run( agents_md = (project_root / "AGENTS.md").read_text(encoding="utf-8") assert "CrewAI Flow declaration" in agents_md assert "schema: crewai.flow/v1" in agents_md - assert 'text(root, "path", "default")' in agents_md + assert "do not assemble the string with CEL `+`" in agents_md + assert "Agent prompt template. Insert Flow values with `${...}`" in agents_md + assert "Runtime inputs passed to the Crew" in agents_md assert "call: expression" in agents_md assert "call: tool" not in agents_md assert "call: script" not in agents_md diff --git a/lib/crewai/src/crewai/flow/expressions.py b/lib/crewai/src/crewai/flow/expressions.py index 3897e3293..5fd9eb522 100644 --- a/lib/crewai/src/crewai/flow/expressions.py +++ b/lib/crewai/src/crewai/flow/expressions.py @@ -11,9 +11,6 @@ from crewai.utilities.serialization import to_serializable if TYPE_CHECKING: - from celpy.celtypes import StringType - from celpy.evaluation import CELFunction - from crewai.flow.runtime import Flow else: from typing_extensions import TypeAliasType @@ -24,29 +21,6 @@ _CEL_MACROS_WITH_LOCAL_BINDINGS = frozenset( ) -def _handle_text_custom_expression( - root: Any, path: Any, default: Any = "" -) -> StringType: - from celpy.celtypes import StringType - - fallback = StringType("" if default is None else str(default)) - current = root - for part in str(path).split("."): - if current is None: - return fallback - try: - if isinstance(current, list): - current = current[int(part)] - else: - current = current[StringType(part)] - except (KeyError, IndexError, TypeError, ValueError): - return fallback - - if current is None: - return fallback - return StringType(_stringify_cel_value(current)) - - def _stringify_cel_value(value: Any) -> str: from celpy.adapter import CELJSONEncoder @@ -98,21 +72,18 @@ def _parse_template_segments(value: str) -> tuple[str | _ExpressionSegment, ...] return tuple(segments) -_EXPRESSION_FUNCTIONS: dict[str, CELFunction] = { - "text": _handle_text_custom_expression, -} - FLOW_TEMPLATE_EXPRESSION_RULES: tuple[str, ...] = ( "Use `${...}` inside action mapping strings to read Flow data with CEL. " "Example value: `Ticket: ${state.ticket_id}`.", "Use `state` for input data. Use `outputs.step_name` for a completed " "method result.", + "In action mapping strings, keep literal text outside `${...}` and " + "interpolate each Flow value directly. Write `Ticket: ${state.ticket_id}`; " + "do not assemble the string with CEL `+`.", "If a value is only one `${...}` expression, the result keeps its type. " "Use this for numbers, booleans, objects, and lists.", "If the string has other text, the final value is text. Non-text values " "become JSON. `null` becomes empty text.", - 'Use `text(root, "path", "default")` for values that may be missing ' - 'or null. The default is optional and is `""`.', ) FLOW_TEMPLATE_EXPRESSION_CONTRACT = " ".join(FLOW_TEMPLATE_EXPRESSION_RULES) FLOW_TEMPLATE_EXPRESSION_EXAMPLES: dict[str, tuple[dict[str, str], ...]] = { @@ -125,10 +96,6 @@ FLOW_TEMPLATE_EXPRESSION_EXAMPLES: dict[str, tuple[dict[str, str], ...]] = { "title": "Keep a list or number type", "code": 'domains: "${state.domains}"\nlimit: "${state.limit}"', }, - { - "title": "Use a default for missing text", - "code": 'input: "Ticket ${text(state, \\"ticket.id\\", \\"unknown\\")}"', - }, ), "json": ( { @@ -141,14 +108,6 @@ FLOW_TEMPLATE_EXPRESSION_EXAMPLES: dict[str, tuple[dict[str, str], ...]] = { '{\n "domains": "${state.domains}",\n "limit": "${state.limit}"\n}' ), }, - { - "title": "Use a default for missing text", - "code": ( - "{\n" - ' "input": "Ticket ${text(state, \\"ticket.id\\", \\"unknown\\")}"\n' - "}" - ), - }, ), } @@ -374,8 +333,7 @@ class Expression: environment = Environment() program = environment.program( - Expression._compile_cel(expression, environment=environment), - functions=_EXPRESSION_FUNCTIONS, + Expression._compile_cel(expression, environment=environment) ) result = program.evaluate(cast(Context, json_to_cel(context))) return json.loads(json.dumps(result, cls=CELJSONEncoder)) diff --git a/lib/crewai/src/crewai/flow/skill.py b/lib/crewai/src/crewai/flow/skill.py index efde51268..ad2862197 100644 --- a/lib/crewai/src/crewai/flow/skill.py +++ b/lib/crewai/src/crewai/flow/skill.py @@ -11,7 +11,6 @@ from jinja2 import Environment, FileSystemLoader import yaml from crewai.flow.expressions import ( - FLOW_TEMPLATE_EXPRESSION_CONTRACT, FLOW_TEMPLATE_EXPRESSION_EXAMPLES, FLOW_TEMPLATE_EXPRESSION_RULES, ) @@ -186,7 +185,14 @@ MODEL_SPECS: tuple[ModelSpec, ...] = ( hidden=True, ), ModelSpec("FlowScriptActionDefinition", "Action", "methods..do[call=script]"), - ModelSpec("FlowToolActionDefinition", "Action", "methods..do[call=tool]"), + ModelSpec( + "FlowToolActionDefinition", + "Action", + "methods..do[call=tool]", + descriptions={ + "with": "Tool input arguments. Insert Flow values with `${...}`.", + }, + ), ModelSpec( "FlowCrewActionDefinition", "Action", @@ -194,7 +200,7 @@ MODEL_SPECS: tuple[ModelSpec, ...] = ( examples=True, descriptions={ "call": "Action discriminator. Use crew to run an inline Crew definition.", - "inputs": f"Actual kickoff inputs passed to the Crew. {FLOW_TEMPLATE_EXPRESSION_CONTRACT} The evaluated values are available to crew agent and task interpolation as `{{name}}` placeholders; reference each input the crew needs in agent or task text.", + "inputs": "Runtime inputs passed to the Crew. Insert Flow values with `${...}` and reference each input as `{name}` in agent or task text.", }, ), ModelSpec( @@ -262,7 +268,7 @@ MODEL_SPECS: tuple[ModelSpec, ...] = ( hidden=True, examples=True, descriptions={ - "input": f"Input passed to the individual agent kickoff outside of a crew. Use one string. {FLOW_TEMPLATE_EXPRESSION_CONTRACT} When an agent needs multiple fields, write one string with labels and separators, for example `Ticket ID: ${{state.ticket_id}}; Message: ${{state.message}}`.", + "input": "Agent prompt template. Insert Flow values with `${...}`, for example `Ticket: ${state.ticket_id}`.", "llm": "Language model that runs this agent. Use an object when setting LLM options such as `max_tokens`.", "planning_config": "Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution.", }, diff --git a/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 b/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 index 3e57f4e30..987c252e2 100644 --- a/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 +++ b/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 @@ -46,6 +46,7 @@ Pick the simplest action that does the job. - Use `call: tool` for packaged deterministic work: API calls, searches, lookups, scoring, file work, or custom CrewAI tools. {% endif %} - Use `call: agent` for one AI worker that classifies, decides, summarizes, writes, or drafts. Put `role`, `goal`, `backstory`, and `input` under `with`. Do not add an action-level `inputs` map to an agent. +- Repository-backed agents may set `from_repository` and omit inline `role`, `goal`, and `backstory`. Explicitly provided fields override repository values. - Use `call: crew` for coordinated AI work with multiple agents or tasks. Define the crew under `with`. Pass runtime values with the action-level `inputs` map. {% if include_each_action %} - Use `call: each` when the same ordered mini-pipeline must run once per item. Give every step a `name`. @@ -137,6 +138,7 @@ Dynamic value rules: - Do not use fields outside the declaration schema{% if include_tool_action %} or tool refs shown here{% endif %}. - Do not put more than one action under a method's `do`. - 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. diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index c225dc955..5e878ee7e 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -1348,7 +1348,15 @@ def test_skill_documents_flow_wiring(): assert "```yaml" in skill assert "[Method](#method-methods)" in skill assert 'input: "Reviewed research: ${outputs.research_brief.raw}"' in skill - assert 'text(root, "path", "default")' in skill + assert "do not assemble the string with CEL `+`" in skill + assert "Do not use CEL `+` to build text in action mappings" in skill + assert "Agent prompt template. Insert Flow values with `${...}`" in skill + assert ( + "Repository-backed agents may set `from_repository` and omit inline " + "`role`, `goal`, and `backstory`" in skill + ) + assert "Runtime inputs passed to the Crew" in skill + assert "Tool input arguments. Insert Flow values with `${...}`" in skill assert "trust CrewAI defaults and omit them" in skill assert "#### LLM Definition" in skill assert "`max_tokens` (optional): integer | null; default `null`" in skill diff --git a/lib/crewai/tests/test_flow_from_definition.py b/lib/crewai/tests/test_flow_from_definition.py index 3543f9202..c35893256 100644 --- a/lib/crewai/tests/test_flow_from_definition.py +++ b/lib/crewai/tests/test_flow_from_definition.py @@ -1102,7 +1102,7 @@ methods: ) -def test_tool_action_renders_text_custom_expression_inputs(): +def test_tool_action_renders_interpolated_inputs(): yaml_str = f""" schema: crewai.flow/v1 name: ToolFlow @@ -1112,8 +1112,8 @@ methods: call: tool ref: {__name__}:StaticSearchTool with: - search_query: "${{'Ticket ID: ' + text(state, 'ticket.id') + '; Subject: ' + text(state, 'ticket.subject') + '; Priority: ' + text(state, 'priority', 'unknown') + '; Message: ' + text(state, 'messages.0.body')}}" - prefix: "${{text(state, 'ticket')}}" + search_query: "Ticket ID: ${{state.ticket.id}}; Subject: ${{state.ticket.subject}}; Message: ${{state.messages[0].body}}" + prefix: "${{state.prefix}}" start: true """ @@ -1124,9 +1124,10 @@ methods: inputs={ "ticket": {"id": 123, "subject": None}, "messages": [{"body": "Initial report"}], + "prefix": "ticket", } ) - == '{"id": 123, "subject": null}:Ticket ID: 123; Subject: ; Priority: unknown; Message: Initial report' + == "ticket:Ticket ID: 123; Subject: ; Message: Initial report" ) @@ -1319,7 +1320,7 @@ methods: role: Analyst goal: Answer questions backstory: Knows things. - input: "Ticket ID: ${text(state, 'ticket.id')}; Subject: ${text(state, 'ticket.subject')}" + input: "Ticket ID: ${state.ticket.id}; Subject: ${state.ticket.subject}" start: true """ @@ -2909,37 +2910,6 @@ def test_explicit_cel_fields_accept_expression_markers(): assert Flow.from_declaration(contents=definition).kickoff(inputs={"score": 90}) == "qualified" -def test_expression_action_runs_text_custom_expression(): - definition = FlowDefinition.from_declaration(contents= - { - "schema": "crewai.flow/v1", - "name": "ExpressionFlow", - "methods": { - "summarize": { - "start": True, - "do": { - "call": "expression", - "expr": ( - "'Ticket ID: ' + text(state, 'ticket.id') + " - "'; Tags: ' + text(state, 'tags')" - ), - }, - } - }, - } - ) - - assert ( - Flow.from_declaration(contents=definition).kickoff( - inputs={ - "ticket": {"id": 123}, - "tags": ["urgent", "billing"], - } - ) - == 'Ticket ID: 123; Tags: ["urgent", "billing"]' - ) - - def test_expression_local_context_recurses_into_dataclass_values(): from crewai.flow.expressions import Expression