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 d5849f92a..1d7886bc4 100644 --- a/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md @@ -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..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..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..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..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:`, 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..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:`, 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..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..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..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..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..do[call=tool]`)](#tool-action-methodsdocalltool) | [Crew Action (`methods..do[call=crew]`)](#crew-action-methodsdocallcrew) | [Agent Action (`methods..do[call=agent]`)](#agent-action-methodsdocallagent) | [Expression Action (`methods..do[call=expression]`)](#expression-action-methodsdocallexpression) | [Script Action (`methods..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..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. diff --git a/lib/cli/tests/test_create_flow.py b/lib/cli/tests/test_create_flow.py index 973f69068..8e297a4ad 100644 --- a/lib/cli/tests/test_create_flow.py +++ b/lib/cli/tests/test_create_flow.py @@ -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"}) diff --git a/lib/crewai/src/crewai/flow/expressions.py b/lib/crewai/src/crewai/flow/expressions.py index 8faeb828c..60a363151 100644 --- a/lib/crewai/src/crewai/flow/expressions.py +++ b/lib/crewai/src/crewai/flow/expressions.py @@ -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)) diff --git a/lib/crewai/src/crewai/flow/skill.py b/lib/crewai/src/crewai/flow/skill.py index e6c1b1339..b7489a3d4 100644 --- a/lib/crewai/src/crewai/flow/skill.py +++ b/lib/crewai/src/crewai/flow/skill.py @@ -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"), 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 0db600d22..dd0bc7638 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 @@ -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`. diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index df1453e3b..0853025bd 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -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(): diff --git a/lib/crewai/tests/test_flow_from_definition.py b/lib/crewai/tests/test_flow_from_definition.py index 04d328c99..7c2ba90df 100644 --- a/lib/crewai/tests/test_flow_from_definition.py +++ b/lib/crewai/tests/test_flow_from_definition.py @@ -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