Add text helper for flow CEL prompts (#6404)

CEL string concatenation currently fails when prompt builders read
missing or null fields. This commit adds `text(root, "path", "default")`
custom CEL helper so prompt text can safely read nested state/output
values.
This commit is contained in:
Vinicius Brasil
2026-06-30 16:45:39 -07:00
committed by GitHub
parent b37505bcf9
commit ba2dafdeda
7 changed files with 157 additions and 102 deletions

View File

@@ -21,7 +21,7 @@ Do not include explanatory prose unless the user asks for it.
1. Define `state` first. Use `type: json_schema` and put the JSON Schema inline.
2. Put required input fields in `state.json_schema.required`. Do not rely on `state.default` to make fields required.
3. Add at least one method with `start: true`.
3. Add exactly one method with `start: true`.
4. Add later methods with `listen`.
5. Give each method exactly one `do` action object. Never make `do` a list.
6. Pass data with `${...}` mappings from `state` and completed `outputs`.
@@ -34,12 +34,8 @@ Method names must match `^[A-Za-z_][A-Za-z0-9_]*$`.
Pick the simplest action that does the job.
- Use `call: expression` for simple reads, filters, computed values, and deterministic routing.
- Use `call: tool` for packaged deterministic work: API calls, searches, lookups, scoring, file work, or custom CrewAI tools.
- 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.
- 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.
- Use `call: each` when the same ordered mini-pipeline must run once per item. Give every step a `name`.
- Use `human_feedback` when a method needs a human checkpoint.
- Use `call: script` only for trusted inline Python. Scripts are not sandboxed.
## Wire Methods Explicitly
@@ -49,8 +45,7 @@ Pick the simplest action that does the job.
- Method names and emitted event names share one namespace. Avoid reusing the same string for both unless the user explicitly wants that.
- 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.
- Conditional `listen` values use `and` / `or`, for example `listen: {and: [validated, enriched]}`. They are not CEL.
- Use `start: true` for normal entrypoints. Use conditional `start` only for advanced event-driven starts.
- Use `start: true` for the single entrypoint.
If an agent is a router, make its goal say exactly what to return, for example:
`Return exactly one bare value: approved, rejected, or needs_review. Do not include explanation.`
@@ -59,11 +54,11 @@ Prefer `call: expression` when routing can be computed without an agent.
## CEL And Dynamic Values
CEL is the expression language for reading Flow data and making small decisions.
Use tools, agents and crews, or trusted scripts for larger work or side effects.
Use agents and crews for larger work or side effects.
Use these expression forms correctly:
- Raw CEL: use in `expr`, `in`, and `if`. Do not wrap raw CEL in `${...}`.
- Raw CEL: use in `expr`. Do not wrap raw CEL in `${...}`.
- Action mappings: wrap the whole string in `${...}`.
- 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.
@@ -73,14 +68,13 @@ Available CEL variables:
- `state`: initial input data, for example `state.ticket.subject`.
- `outputs`: completed method outputs, for example `outputs.classify_ticket`.
- `item`: the current item inside `each`, for example `item.company_domain`.
- `outputs`: inside `each`, prior step outputs for the current item, for example `outputs.enrich`.
Dynamic value rules:
- A string is dynamic only when the whole trimmed string is wrapped in `${...}`.
- `Ticket: ${state.ticket}` stays literal. For computed strings, build the string inside the CEL expression.
- When an agent needs multiple fields, build one single-line CEL string with labels and separators. Example: `input: "${'Ticket ID: ' + state.ticket_id + '; Message: ' + state.message}"`.
- For prompt text, use CrewAI's CEL helper `text(root, "path", "default")` to safely read missing or null values as strings. The default argument is optional and defaults to `""`.
- When an agent needs multiple fields, build one single-line CEL string with labels and separators. Example: `input: "${'Ticket ID: ' + text(state, 'ticket_id') + '; Message: ' + text(state, 'message')}"`.
- In YAML, do not put `\n` escapes inside `${...}` strings. Prefer plain `${state.field}` values or the single-line labeled pattern above.
- `${...}` keeps the value type. It does not always make text.
- Crew action-level `inputs` are the actual Crew kickoff inputs. Use CEL-wrapped values there for runtime data from `state` or `outputs`.
@@ -91,23 +85,19 @@ Dynamic value rules:
- Agent outputs may also be objects. Use fields like `${outputs.classify_ticket.raw}` or `${outputs.classify_ticket.pydantic.category}`.
- Use `with.inputs` only for static Crew input defaults.
- Agent action `with.input` is the agent's single input value.
- Trusted `script` actions can mutate state explicitly. Use them only when the user asks for that behavior.
## Do Not
- Do not invent top-level keys outside the Flow declaration shape.
- Do not use fields outside the declaration schema or tool refs shown here.
- 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 reference `outputs.some_method` before `some_method` can run.
- Do not use the same string for an emitted event and a method name unless the user asks for it.
- Do not use `emit` without `router: true` or `human_feedback.emit`.
- 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.
- Do not set `config.stream: true` unless the caller is expected to consume a streaming result. For normal generated flows and CLI smoke tests, omit it.
- Do not put conversational settings under `state`, `config`, or a method. Use top-level `conversational` only when the user asks for a chat or conversation flow.
- Do not use `each` without at least one named step.
- Do not use `script` for untrusted input or user-authored code.
## Examples
@@ -196,9 +186,6 @@ Fields:
- `name` (required): string. Unique flow name used in logs, events, and traces.
- `description` (optional): string | null; default `null`. Human-readable summary of the flow.
- `state` (required): [State](#json-schema-state-statetypejson_schema). State contract for the initial state and updates during execution.
- `config` (optional): [Config (`config`)](#config-config); default generated default. Serializable flow-level execution configuration.
- `persist` (optional): [Persistence (`persist`)](#persistence-persist) | null; default `null`. Flow-level persistence configuration.
- `conversational` (optional): object | null; default `null`. Top-level conversational flow configuration, only when the flow supports chat.
- `methods` (required): map of string to [Method](#method-methods). Mapping of method names to method definitions.
### JSON Schema State (`state[type=json_schema]`)
@@ -216,44 +203,19 @@ Fields:
Fields:
- `description` (optional): string | null; default `null`. Human-readable summary of what this method does.
- `do` (required): [Action](#action). Single action object executed when this method runs.
- `start` (optional): boolean | string | map of string to any | null; default `null`. Marks a start method. Use `true` for the normal entrypoint. String or map conditions are advanced trigger conditions; use them only when the user asks for event/condition-based starts.
- `listen` (optional): string | map of string to any | null; default `null`. Trigger condition that runs this method after upstream events. A string target can be a method name or a router-emitted event name, and both live in the same trigger namespace. Map conditions are for `and`/`or` trigger composition, for example `{"and": ["validated", "processed"]}`.
- `start` (optional): boolean | string | map of string to any | null; default `null`. Marks the single normal entrypoint. Use `true`.
- `listen` (optional): string | map of string to any | null; default `null`. Runs this method after one upstream method or router-emitted event.
- `router` (optional): boolean; default `false`. Whether the method output should be treated as the next event name. Router actions must return one event name string, with no surrounding explanation.
- `emit` (optional): list[string] | null; default `null`. Declared router events this method may emit. Each emitted event name should be unique and should not collide with method names.
- `human_feedback` (optional): [Human Feedback (`methods.<name>.human_feedback`)](#human-feedback-methodshuman_feedback) | null; default `null`. Optional human feedback step applied after the method action.
- `persist` (optional): [Persistence (`persist`)](#persistence-persist) | null; default `null`. Method-level persistence override.
### Action
Discriminated union by `call`.
Allowed shapes:
- [`call: script`](#script-action-methodsdocallscript)
- [`call: tool`](#tool-action-methodsdocalltool)
- [`call: crew`](#crew-action-methodsdocallcrew)
- [`call: agent`](#agent-action-methodsdocallagent)
- [`call: expression`](#expression-action-methodsdocallexpression)
- [`call: each`](#each-action-methodsdocalleach)
### Script Action (`methods.<name>.do[call=script]`)
Shape:
- `call: script`
Fields:
- `call` (required): must be `script`. Action discriminator. Use script to execute trusted inline Python.
- `code` (required): string. Trusted inline Python source. Values are available as state and outputs; they are not interpolated into the source. This is not sandboxed.
- `language` (optional): must be `python`; default `python`. Script language. Only python is currently supported.
### Tool Action (`methods.<name>.do[call=tool]`)
Shape:
- `call: tool`
Fields:
- `call` (required): must be `tool`. Action discriminator. Use tool to instantiate and run a CrewAI tool.
- `ref` (required): string. Reference to the CrewAI tool to run.
- `with` (optional): map of string to expression data | null; default `null`. Tool input arguments. String values are evaluated as CEL only when the trimmed value starts with ${ and ends with }; all other values are literal.
### Crew Action (`methods.<name>.do[call=crew]`)
@@ -279,6 +241,9 @@ Fields:
- `goal` (required): string. Crew agent goal. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}`
- `backstory` (required): string. Crew agent backstory. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Expert at concise technical research.`
- `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}`
- `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"}]`
#### Crew Task Definition (`methods.<name>.do[call=crew].with.tasks[]`)
@@ -304,7 +269,10 @@ Fields:
- `goal` (required): string. Individual agent goal for the Flow agent action outside of a crew. Example: `Draft a concise customer reply`
- `backstory` (required): string. Individual agent backstory used to shape behavior outside of a crew. Example: `Expert at resolving SaaS support questions.`
- `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}`
- `input` (required): string. Input passed to the individual agent kickoff outside of a crew. Use a single string value, often a dynamic `${...}` expression. When an agent needs multiple fields, build one single-line CEL string with labels and separators, for example `${'Ticket ID: ' + state.ticket_id + '; Message: ' + state.message}`. In YAML, avoid `\n` escapes inside `${...}` strings. Example: `${state.ticket.body}`
- `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 a single string value, often a dynamic `${...}` expression. When an agent needs multiple fields, build one single-line CEL string with labels and separators, using `text(root, 'path')` for values that may be missing or null, for example `${'Ticket ID: ' + text(state, 'ticket_id') + '; Message: ' + text(state, 'message')}`. In YAML, avoid `\n` escapes inside `${...}` strings. Example: `${state.ticket.body}`
### Expression Action (`methods.<name>.do[call=expression]`)
@@ -315,55 +283,6 @@ Fields:
- `call` (required): must be `expression`. Action discriminator. Use expression to evaluate a CEL expression.
- `expr` (required): string. CEL expression evaluated against state, outputs, and local context.
### Each Action (`methods.<name>.do[call=each]`)
Shape:
- `call: each`
Fields:
- `call` (required): must be `each`. Action discriminator. Use each to run a sequence of actions for every item in an input list.
- `in` (required): string. CEL expression that must evaluate to the list to iterate.
- `do` (required): list of [Each Step (`methods.<name>.do[call=each].do[]`)](#each-step-methodsdocalleachdo). Ordered steps to run for each item. Each step has a name, optional if expression, and atomic action.
### Each Step (`methods.<name>.do[call=each].do[]`)
Fields:
- `name` (required): string. Step name used to reference this step's output.
- `if` (optional): string | null; default `null`. Optional CEL expression evaluated against state, outputs, and local context. When present, the step runs only if the expression evaluates to true.
- `action` (required): [Tool Action (`methods.<name>.do[call=tool]`)](#tool-action-methodsdocalltool) | [Crew Action (`methods.<name>.do[call=crew]`)](#crew-action-methodsdocallcrew) | [Agent Action (`methods.<name>.do[call=agent]`)](#agent-action-methodsdocallagent) | [Expression Action (`methods.<name>.do[call=expression]`)](#expression-action-methodsdocallexpression) | [Script Action (`methods.<name>.do[call=script]`)](#script-action-methodsdocallscript). Atomic action to run for this step.
### Config (`config`)
Fields:
- `tracing` (optional): boolean | null; default `null`. Override for flow tracing; when omitted, execution defaults apply.
- `stream` (optional): boolean; default `false`. Whether the flow should emit streaming events when supported.
- `memory` (optional): map of string to any | null; default `null`. Serializable memory configuration passed to flow execution.
- `input_provider` (optional): string | null; default `null`. Provider key used to supply initial state.
- `suppress_flow_events` (optional): boolean; default `false`. Disable flow event emission for this definition.
- `max_method_calls` (optional): integer; default `100`. Maximum number of method executions allowed during one kickoff.
- `defer_trace_finalization` (optional): boolean; default `false`. Defer trace finalization so callers can complete tracing later.
- `checkpoint` (optional): boolean | map of string to any | null; default `null`. Checkpointing configuration, or true to use default checkpointing.
### Persistence (`persist`)
Fields:
- `enabled` (optional): boolean; default `false`. Whether persistence is enabled for this flow or method.
- `verbose` (optional): boolean; default `false`. Whether persistence should emit verbose diagnostic output.
- `persistence` (optional): any; default `null`. Persistence backend configuration or import reference.
### Human Feedback (`methods.<name>.human_feedback`)
Fields:
- `message` (required): string. Prompt shown to the human reviewer when feedback is requested.
- `emit` (optional): list[string] | null; default `null`. Allowed feedback outcomes. When set, the method routes like a router using the selected outcome.
- `llm` (optional): any; default `gpt-4o-mini`. LLM configuration used to assist or process human feedback.
- `default_outcome` (optional): string | null; default `null`. Outcome to use when feedback cannot be collected.
- `metadata` (optional): map of string to any | null; default `null`. Serializable metadata attached to the feedback request.
- `provider` (optional): any; default `null`. Feedback provider configuration or import reference.
- `learn` (optional): boolean; default `false`. Whether feedback should be recorded for later learning workflows.
- `learn_source` (optional): string; default `hitl`. Source label attached to learned feedback records.
- `learn_strict` (optional): boolean; default `false`. Whether learning should enforce strict validation of feedback data.
### Cross-Field Rules
- A method has exactly one `do` action object with one `call` discriminator.
@@ -372,4 +291,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}`.
- `each.do` must contain at least one named step.

View File

@@ -29,6 +29,12 @@ 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 "call: expression" in agents_md
assert "call: tool" not in agents_md
assert "call: script" not in agents_md
assert "call: each" not in agents_md
assert "human_feedback" not in agents_md
monkeypatch.chdir(project_root)
result = CliRunner().invoke(crewai, ["run"], env={"UV_RUN_RECURSION_DEPTH": "1"})

View File

@@ -10,6 +10,9 @@ 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
@@ -18,6 +21,37 @@ else:
_CEL_MACROS_WITH_LOCAL_BINDINGS = frozenset(
{"all", "exists", "exists_one", "filter", "map"}
)
def _handle_text_custom_expression(
root: Any, path: Any, default: Any = ""
) -> StringType:
from celpy.adapter import CELJSONEncoder
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
if isinstance(current, str):
return StringType(current)
return StringType(json.dumps(current, cls=CELJSONEncoder, ensure_ascii=False))
_EXPRESSION_FUNCTIONS: dict[str, CELFunction] = {
"text": _handle_text_custom_expression,
}
if TYPE_CHECKING:
ExpressionData: TypeAlias = (
str
@@ -219,7 +253,8 @@ class Expression:
environment = Environment()
program = environment.program(
Expression._compile_cel(expression, environment=environment)
Expression._compile_cel(expression, environment=environment),
functions=_EXPRESSION_FUNCTIONS,
)
result = program.evaluate(cast(Context, json_to_cel(context)))
return json.loads(json.dumps(result, cls=CELJSONEncoder))

View File

@@ -240,7 +240,7 @@ MODEL_SPECS: tuple[ModelSpec, ...] = (
hidden=True,
examples=True,
descriptions={
"input": "Input passed to the individual agent kickoff outside of a crew. Use a single string value, often a dynamic `${...}` expression. When an agent needs multiple fields, build one single-line CEL string with labels and separators, for example `${'Ticket ID: ' + state.ticket_id + '; Message: ' + state.message}`. In YAML, avoid `\\n` escapes inside `${...}` strings.",
"input": "Input passed to the individual agent kickoff outside of a crew. Use a single string value, often a dynamic `${...}` expression. When an agent needs multiple fields, build one single-line CEL string with labels and separators, using `text(root, 'path')` for values that may be missing or null, for example `${'Ticket ID: ' + text(state, 'ticket_id') + '; Message: ' + text(state, 'message')}`. In YAML, avoid `\\n` escapes inside `${...}` strings.",
},
),
ModelSpec("FlowConfigDefinition", "Config", "config"),

View File

@@ -104,7 +104,8 @@ Dynamic value rules:
- A string is dynamic only when the whole trimmed string is wrapped in `${...}`.
- `Ticket: ${state.ticket}` stays literal. For computed strings, build the string inside the CEL expression.
- When an agent needs multiple fields, build one single-line CEL string with labels and separators. Example: `input: "${'Ticket ID: ' + state.ticket_id + '; Message: ' + state.message}"`.
- For prompt text, use CrewAI's CEL helper `text(root, "path", "default")` to safely read missing or null values as strings. The default argument is optional and defaults to `""`.
- When an agent needs multiple fields, build one single-line CEL string with labels and separators. Example: `input: "${'Ticket ID: ' + text(state, 'ticket_id') + '; Message: ' + text(state, 'message')}"`.
- In YAML, do not put `\n` escapes inside `${...}` strings. Prefer plain `${state.field}` values or the single-line labeled pattern above.
- `${...}` keeps the value type. It does not always make text.
- Crew action-level `inputs` are the actual Crew kickoff inputs. Use CEL-wrapped values there for runtime data from `state` or `outputs`.

View File

@@ -1309,6 +1309,7 @@ def test_skill_documents_flow_wiring():
assert isinstance(skill, str)
assert "```yaml" in skill
assert "[Method](#method-methods)" in skill
assert 'text(root, "path", "default")' in skill
def test_skill_can_render_json_examples():

View File

@@ -848,6 +848,34 @@ methods:
)
def test_tool_action_renders_text_custom_expression_inputs():
yaml_str = f"""
schema: crewai.flow/v1
name: ToolFlow
methods:
search:
do:
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')}}"
start: true
"""
flow = Flow.from_declaration(contents=yaml_str)
assert (
flow.kickoff(
inputs={
"ticket": {"id": 123, "subject": None},
"messages": [{"body": "Initial report"}],
}
)
== '{"id": 123, "subject": null}:Ticket ID: 123; Subject: ; Priority: unknown; Message: Initial report'
)
def test_agent_action_runs_inline_yaml_definition(monkeypatch: pytest.MonkeyPatch):
from crewai import Agent
@@ -881,6 +909,41 @@ methods:
}
def test_agent_action_renders_text_custom_expression_input(
monkeypatch: pytest.MonkeyPatch,
):
from crewai import Agent
async def fake_kickoff_async(
self: Agent, messages: str, **_kwargs: Any
) -> dict[str, Any]:
return {"agent": self.role, "input": messages}
monkeypatch.setattr(Agent, "kickoff_async", fake_kickoff_async)
yaml_str = """
schema: crewai.flow/v1
name: AgentFlow
methods:
answer:
do:
call: agent
with:
role: Analyst
goal: Answer questions
backstory: Knows things.
input: "${'Ticket ID: ' + text(state, 'ticket.id') + '; Subject: ' + text(state, 'ticket.subject')}"
start: true
"""
flow = Flow.from_declaration(contents=yaml_str)
assert flow.kickoff(inputs={"ticket": {"id": 123, "subject": None}}) == {
"agent": "Analyst",
"input": "Ticket ID: 123; Subject: ",
}
def test_agent_action_runs_inside_each(monkeypatch: pytest.MonkeyPatch):
from crewai import Agent
@@ -2150,6 +2213,37 @@ 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