Remove redundant CEL text helper (#6528)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled

This commit removes the redundant CEL text helper, in favor of the
easier interpolation syntax.
This commit is contained in:
Vinicius Brasil
2026-07-13 14:29:36 -04:00
committed by GitHub
parent fb8e93be25
commit 9d72e269e4
7 changed files with 38 additions and 98 deletions

View File

@@ -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.<name>.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:<name>`, 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}`.

View File

@@ -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

View File

@@ -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))

View File

@@ -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.<name>.do[call=script]"),
ModelSpec("FlowToolActionDefinition", "Action", "methods.<name>.do[call=tool]"),
ModelSpec(
"FlowToolActionDefinition",
"Action",
"methods.<name>.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.",
},

View File

@@ -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.

View File

@@ -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

View File

@@ -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