Skip to content

Commit 9d72e26

Browse files
authored
Remove redundant CEL text helper (#6528)
This commit removes the redundant CEL text helper, in favor of the easier interpolation syntax.
1 parent fb8e93b commit 9d72e26

7 files changed

Lines changed: 38 additions & 98 deletions

File tree

lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ Use these expression forms correctly:
5959
- Raw CEL: use in `expr`. Do not wrap raw CEL in `${...}`.
6060
- Use `${...}` inside action mapping strings to read Flow data with CEL. Example value: `Ticket: ${state.ticket_id}`.
6161
- Use `state` for input data. Use `outputs.step_name` for a completed method result.
62+
- 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 `+`.
6263
- If a value is only one `${...}` expression, the result keeps its type. Use this for numbers, booleans, objects, and lists.
6364
- If the string has other text, the final value is text. Non-text values become JSON. `null` becomes empty text.
64-
- Use `text(root, "path", "default")` for values that may be missing or null. The default is optional and is `""`.
6565

6666
Expression examples:
6767

@@ -78,12 +78,6 @@ domains: "${state.domains}"
7878
limit: "${state.limit}"
7979
```
8080
81-
Use a default for missing text:
82-
83-
```yaml
84-
input: "Ticket ${text(state, \"ticket.id\", \"unknown\")}"
85-
```
86-
8781
- Crew text: use `{name}` placeholders from crew inputs. Example: `Research {topic}`.
8882
- Crew inputs become prompt text only when agent or task text references matching `{name}` placeholders.
8983
- 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:
111105
- Do not use fields outside the declaration schema.
112106
- Do not put more than one action under a method's `do`.
113107
- Do not make `do` a list.
108+
- Do not use CEL `+` to build text in action mappings. Keep the text literal and insert each dynamic value with `${...}`.
114109
- Do not reference `outputs.some_method` before `some_method` can run.
115110
- Do not set a method's `listen` to its own method name.
116111
- 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:
246241
Fields:
247242
- `call` (required): must be `crew`. Action discriminator. Use crew to run an inline Crew definition. Example: `crew`
248243
- `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"}]}`
249-
- `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}"}`
244+
- `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}"}`
250245

251246
#### Crew Definition (`methods.<name>.do[call=crew].with`)
252247

@@ -311,7 +306,7 @@ Fields:
311306
- `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"]`
312307
- `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"]`
313308
- `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"}]`
314-
- `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}`
309+
- `input` (required): string. Agent prompt template. Insert Flow values with `${...}`, for example `Ticket: ${state.ticket_id}`. Example: `${state.ticket.body}`
315310

316311
#### LLM Definition
317312

@@ -349,4 +344,3 @@ Fields:
349344
- Crew action-level `inputs` are the Crew kickoff inputs; use CEL-wrapped strings there for runtime values.
350345
- Crew agent/task interpolation uses `{name}` placeholders from evaluated crew inputs.
351346
- Agent `with.input` must be text. Use `${outputs.method_name.raw}` or a text field like `${outputs.method_name.json_dict.summary}`.
352-

lib/cli/tests/test_create_flow.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ def test_create_flow_declarative_project_can_run(
2929
agents_md = (project_root / "AGENTS.md").read_text(encoding="utf-8")
3030
assert "CrewAI Flow declaration" in agents_md
3131
assert "schema: crewai.flow/v1" in agents_md
32-
assert 'text(root, "path", "default")' in agents_md
32+
assert "do not assemble the string with CEL `+`" in agents_md
33+
assert "Agent prompt template. Insert Flow values with `${...}`" in agents_md
34+
assert "Runtime inputs passed to the Crew" in agents_md
3335
assert "call: expression" in agents_md
3436
assert "call: tool" not in agents_md
3537
assert "call: script" not in agents_md

lib/crewai/src/crewai/flow/expressions.py

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111

1212

1313
if TYPE_CHECKING:
14-
from celpy.celtypes import StringType
15-
from celpy.evaluation import CELFunction
16-
1714
from crewai.flow.runtime import Flow
1815
else:
1916
from typing_extensions import TypeAliasType
@@ -24,29 +21,6 @@
2421
)
2522

2623

27-
def _handle_text_custom_expression(
28-
root: Any, path: Any, default: Any = ""
29-
) -> StringType:
30-
from celpy.celtypes import StringType
31-
32-
fallback = StringType("" if default is None else str(default))
33-
current = root
34-
for part in str(path).split("."):
35-
if current is None:
36-
return fallback
37-
try:
38-
if isinstance(current, list):
39-
current = current[int(part)]
40-
else:
41-
current = current[StringType(part)]
42-
except (KeyError, IndexError, TypeError, ValueError):
43-
return fallback
44-
45-
if current is None:
46-
return fallback
47-
return StringType(_stringify_cel_value(current))
48-
49-
5024
def _stringify_cel_value(value: Any) -> str:
5125
from celpy.adapter import CELJSONEncoder
5226

@@ -98,21 +72,18 @@ def _parse_template_segments(value: str) -> tuple[str | _ExpressionSegment, ...]
9872
return tuple(segments)
9973

10074

101-
_EXPRESSION_FUNCTIONS: dict[str, CELFunction] = {
102-
"text": _handle_text_custom_expression,
103-
}
104-
10575
FLOW_TEMPLATE_EXPRESSION_RULES: tuple[str, ...] = (
10676
"Use `${...}` inside action mapping strings to read Flow data with CEL. "
10777
"Example value: `Ticket: ${state.ticket_id}`.",
10878
"Use `state` for input data. Use `outputs.step_name` for a completed "
10979
"method result.",
80+
"In action mapping strings, keep literal text outside `${...}` and "
81+
"interpolate each Flow value directly. Write `Ticket: ${state.ticket_id}`; "
82+
"do not assemble the string with CEL `+`.",
11083
"If a value is only one `${...}` expression, the result keeps its type. "
11184
"Use this for numbers, booleans, objects, and lists.",
11285
"If the string has other text, the final value is text. Non-text values "
11386
"become JSON. `null` becomes empty text.",
114-
'Use `text(root, "path", "default")` for values that may be missing '
115-
'or null. The default is optional and is `""`.',
11687
)
11788
FLOW_TEMPLATE_EXPRESSION_CONTRACT = " ".join(FLOW_TEMPLATE_EXPRESSION_RULES)
11889
FLOW_TEMPLATE_EXPRESSION_EXAMPLES: dict[str, tuple[dict[str, str], ...]] = {
@@ -125,10 +96,6 @@ def _parse_template_segments(value: str) -> tuple[str | _ExpressionSegment, ...]
12596
"title": "Keep a list or number type",
12697
"code": 'domains: "${state.domains}"\nlimit: "${state.limit}"',
12798
},
128-
{
129-
"title": "Use a default for missing text",
130-
"code": 'input: "Ticket ${text(state, \\"ticket.id\\", \\"unknown\\")}"',
131-
},
13299
),
133100
"json": (
134101
{
@@ -141,14 +108,6 @@ def _parse_template_segments(value: str) -> tuple[str | _ExpressionSegment, ...]
141108
'{\n "domains": "${state.domains}",\n "limit": "${state.limit}"\n}'
142109
),
143110
},
144-
{
145-
"title": "Use a default for missing text",
146-
"code": (
147-
"{\n"
148-
' "input": "Ticket ${text(state, \\"ticket.id\\", \\"unknown\\")}"\n'
149-
"}"
150-
),
151-
},
152111
),
153112
}
154113

@@ -374,8 +333,7 @@ def _evaluate_cel(expression: str, context: dict[str, Any]) -> Any:
374333

375334
environment = Environment()
376335
program = environment.program(
377-
Expression._compile_cel(expression, environment=environment),
378-
functions=_EXPRESSION_FUNCTIONS,
336+
Expression._compile_cel(expression, environment=environment)
379337
)
380338
result = program.evaluate(cast(Context, json_to_cel(context)))
381339
return json.loads(json.dumps(result, cls=CELJSONEncoder))

lib/crewai/src/crewai/flow/skill.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import yaml
1212

1313
from crewai.flow.expressions import (
14-
FLOW_TEMPLATE_EXPRESSION_CONTRACT,
1514
FLOW_TEMPLATE_EXPRESSION_EXAMPLES,
1615
FLOW_TEMPLATE_EXPRESSION_RULES,
1716
)
@@ -186,15 +185,22 @@ def display_label(self) -> str:
186185
hidden=True,
187186
),
188187
ModelSpec("FlowScriptActionDefinition", "Action", "methods.<name>.do[call=script]"),
189-
ModelSpec("FlowToolActionDefinition", "Action", "methods.<name>.do[call=tool]"),
188+
ModelSpec(
189+
"FlowToolActionDefinition",
190+
"Action",
191+
"methods.<name>.do[call=tool]",
192+
descriptions={
193+
"with": "Tool input arguments. Insert Flow values with `${...}`.",
194+
},
195+
),
190196
ModelSpec(
191197
"FlowCrewActionDefinition",
192198
"Action",
193199
"methods.<name>.do[call=crew]",
194200
examples=True,
195201
descriptions={
196202
"call": "Action discriminator. Use crew to run an inline Crew definition.",
197-
"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.",
203+
"inputs": "Runtime inputs passed to the Crew. Insert Flow values with `${...}` and reference each input as `{name}` in agent or task text.",
198204
},
199205
),
200206
ModelSpec(
@@ -262,7 +268,7 @@ def display_label(self) -> str:
262268
hidden=True,
263269
examples=True,
264270
descriptions={
265-
"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}}`.",
271+
"input": "Agent prompt template. Insert Flow values with `${...}`, for example `Ticket: ${state.ticket_id}`.",
266272
"llm": "Language model that runs this agent. Use an object when setting LLM options such as `max_tokens`.",
267273
"planning_config": "Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution.",
268274
},

lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Pick the simplest action that does the job.
4646
- Use `call: tool` for packaged deterministic work: API calls, searches, lookups, scoring, file work, or custom CrewAI tools.
4747
{% endif %}
4848
- 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.
49+
- Repository-backed agents may set `from_repository` and omit inline `role`, `goal`, and `backstory`. Explicitly provided fields override repository values.
4950
- 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.
5051
{% if include_each_action %}
5152
- 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:
137138
- Do not use fields outside the declaration schema{% if include_tool_action %} or tool refs shown here{% endif %}.
138139
- Do not put more than one action under a method's `do`.
139140
- Do not make `do` a list.
141+
- Do not use CEL `+` to build text in action mappings. Keep the text literal and insert each dynamic value with `${...}`.
140142
- Do not reference `outputs.some_method` before `some_method` can run.
141143
- Do not set a method's `listen` to its own method name.
142144
- Do not use the same string for an emitted event and a method name unless the user asks for it.

lib/crewai/tests/test_flow_definition.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,7 +1348,15 @@ def test_skill_documents_flow_wiring():
13481348
assert "```yaml" in skill
13491349
assert "[Method](#method-methods)" in skill
13501350
assert 'input: "Reviewed research: ${outputs.research_brief.raw}"' in skill
1351-
assert 'text(root, "path", "default")' in skill
1351+
assert "do not assemble the string with CEL `+`" in skill
1352+
assert "Do not use CEL `+` to build text in action mappings" in skill
1353+
assert "Agent prompt template. Insert Flow values with `${...}`" in skill
1354+
assert (
1355+
"Repository-backed agents may set `from_repository` and omit inline "
1356+
"`role`, `goal`, and `backstory`" in skill
1357+
)
1358+
assert "Runtime inputs passed to the Crew" in skill
1359+
assert "Tool input arguments. Insert Flow values with `${...}`" in skill
13521360
assert "trust CrewAI defaults and omit them" in skill
13531361
assert "#### LLM Definition" in skill
13541362
assert "`max_tokens` (optional): integer | null; default `null`" in skill

lib/crewai/tests/test_flow_from_definition.py

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,7 @@ def test_tool_action_interpolates_values_inside_list_inputs():
11021102
)
11031103

11041104

1105-
def test_tool_action_renders_text_custom_expression_inputs():
1105+
def test_tool_action_renders_interpolated_inputs():
11061106
yaml_str = f"""
11071107
schema: crewai.flow/v1
11081108
name: ToolFlow
@@ -1112,8 +1112,8 @@ def test_tool_action_renders_text_custom_expression_inputs():
11121112
call: tool
11131113
ref: {__name__}:StaticSearchTool
11141114
with:
1115-
search_query: "${{'Ticket ID: ' + text(state, 'ticket.id') + '; Subject: ' + text(state, 'ticket.subject') + '; Priority: ' + text(state, 'priority', 'unknown') + '; Message: ' + text(state, 'messages.0.body')}}"
1116-
prefix: "${{text(state, 'ticket')}}"
1115+
search_query: "Ticket ID: ${{state.ticket.id}}; Subject: ${{state.ticket.subject}}; Message: ${{state.messages[0].body}}"
1116+
prefix: "${{state.prefix}}"
11171117
start: true
11181118
"""
11191119

@@ -1124,9 +1124,10 @@ def test_tool_action_renders_text_custom_expression_inputs():
11241124
inputs={
11251125
"ticket": {"id": 123, "subject": None},
11261126
"messages": [{"body": "Initial report"}],
1127+
"prefix": "ticket",
11271128
}
11281129
)
1129-
== '{"id": 123, "subject": null}:Ticket ID: 123; Subject: ; Priority: unknown; Message: Initial report'
1130+
== "ticket:Ticket ID: 123; Subject: ; Message: Initial report"
11301131
)
11311132

11321133

@@ -1319,7 +1320,7 @@ async def fake_kickoff_async(
13191320
role: Analyst
13201321
goal: Answer questions
13211322
backstory: Knows things.
1322-
input: "Ticket ID: ${text(state, 'ticket.id')}; Subject: ${text(state, 'ticket.subject')}"
1323+
input: "Ticket ID: ${state.ticket.id}; Subject: ${state.ticket.subject}"
13231324
start: true
13241325
"""
13251326

@@ -2909,37 +2910,6 @@ def test_explicit_cel_fields_accept_expression_markers():
29092910
assert Flow.from_declaration(contents=definition).kickoff(inputs={"score": 90}) == "qualified"
29102911

29112912

2912-
def test_expression_action_runs_text_custom_expression():
2913-
definition = FlowDefinition.from_declaration(contents=
2914-
{
2915-
"schema": "crewai.flow/v1",
2916-
"name": "ExpressionFlow",
2917-
"methods": {
2918-
"summarize": {
2919-
"start": True,
2920-
"do": {
2921-
"call": "expression",
2922-
"expr": (
2923-
"'Ticket ID: ' + text(state, 'ticket.id') + "
2924-
"'; Tags: ' + text(state, 'tags')"
2925-
),
2926-
},
2927-
}
2928-
},
2929-
}
2930-
)
2931-
2932-
assert (
2933-
Flow.from_declaration(contents=definition).kickoff(
2934-
inputs={
2935-
"ticket": {"id": 123},
2936-
"tags": ["urgent", "billing"],
2937-
}
2938-
)
2939-
== 'Ticket ID: 123; Tags: ["urgent", "billing"]'
2940-
)
2941-
2942-
29432913
def test_expression_local_context_recurses_into_dataclass_values():
29442914
from crewai.flow.expressions import Expression
29452915

0 commit comments

Comments
 (0)