diff --git a/lib/cli/src/crewai_cli/create_flow.py b/lib/cli/src/crewai_cli/create_flow.py index 678228f65..7921c2847 100644 --- a/lib/cli/src/crewai_cli/create_flow.py +++ b/lib/cli/src/crewai_cli/create_flow.py @@ -126,10 +126,7 @@ def _create_declarative_flow( package_dir = Path(__file__).parent templates_dir = package_dir / "templates" / "declarative_flow" - - agents_md_src = package_dir / "templates" / "AGENTS.md" - if agents_md_src.exists(): - shutil.copy2(agents_md_src, project_root / "AGENTS.md") + root_template_files = {".gitignore", "AGENTS.md", "README.md", "pyproject.toml"} for src_file in templates_dir.rglob("*"): if not src_file.is_file(): @@ -138,7 +135,7 @@ def _create_declarative_flow( relative_path = src_file.relative_to(templates_dir) dst_file = ( project_root / relative_path - if relative_path.name in {".gitignore", "README.md", "pyproject.toml"} + if relative_path.name in root_template_files else package_root / relative_path ) dst_file.parent.mkdir(parents=True, exist_ok=True) diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md new file mode 100644 index 000000000..d5849f92a --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md @@ -0,0 +1,375 @@ +--- +name: flow-definition +description: Create or edit CrewAI Flow declarations. Use when the user needs a YAML or JSON flow with methods, state, agents, crews, tools, outputs, or conditional branches. +--- + +# Flow Definition + +You are writing a CrewAI Flow declaration for the user. +Use these instructions when the user asks you to create or edit a Flow. +Return one valid `crewai.flow/v1` YAML or JSON document. + +Treat this document as instructions for you, not as text to show the user. +Follow the examples for shape and formatting, then use the API reference to check exact fields. + +## Output Format + +Return one valid `crewai.flow/v1` Flow declaration. +Do not include explanatory prose unless the user asks for it. + +## Build It In This Order + +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`. +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`. +7. Before final output, check every `listen`, `emit`, and `outputs.some_method` reference. + +Method names must match `^[A-Za-z_][A-Za-z0-9_]*$`. + +## Choose One Action Per Method + +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 + +- `state` is the initial shared data shape. Action results do not automatically merge into `state`. +- Read method results with `outputs.method_name` after that method can run. +- `listen` targets a method name or a router-emitted event name. +- 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. + +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.` +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 these expression forms correctly: + +- Raw CEL: use in `expr`, `in`, and `if`. 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. +- 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`. + +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}"`. +- 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`. +- Crew action-level `inputs` alone are not grounding. Include placeholders for the facts the model must use. +- Crew outputs are objects. Use `${outputs.research_brief.raw}` for text. +- For structured crew output, use fields like `${outputs.research_brief.json_dict.field}` or `${outputs.research_brief.pydantic.field}`. +- Do not pass a whole crew output to an agent input, like `${outputs.research_brief}`. +- 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 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 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 + +### Crew review with routed follow-up + +```yaml +schema: crewai.flow/v1 +name: ResearchReviewFlow +state: + type: json_schema + json_schema: + type: object + properties: + topic: + type: string + audience: + type: string + required: + - topic + - audience + default: + topic: AI agent orchestration + audience: platform engineering leaders +methods: + research_brief: + start: true + do: + call: crew + with: + agents: + researcher: + role: Research analyst + goal: Research {topic} for {audience} + backstory: Expert at concise technical research. + reviewer: + role: Strategy reviewer + goal: Decide whether the research needs an executive follow-up + backstory: Experienced at reviewing technical briefs for leaders. + tasks: + - name: research_task + description: Research {topic} for {audience}. + expected_output: Key findings and tradeoffs. + agent: researcher + - name: review_task + description: Review the research and decide if an executive follow-up is needed. + expected_output: 'A brief review ending with `needs_followup: true` or `needs_followup: false`.' + agent: reviewer + inputs: + topic: Default topic + audience: Default audience + inputs: + topic: "${state.topic}" + audience: "${state.audience}" + route_followup: + listen: research_brief + router: true + emit: + - followup + - done + do: + call: agent + with: + role: Follow-up router + goal: 'Return exactly one bare value: followup or done. Do not include explanation.' + backstory: Skilled at routing reviewed research briefs. + input: "${outputs.research_brief.raw}" + write_followup: + listen: followup + do: + call: agent + with: + role: Executive communications specialist + goal: Draft a concise executive follow-up from the reviewed research + backstory: Writes crisp follow-ups for technical leaders. + input: "${outputs.research_brief.raw}" +``` + +## API Reference + +Use this appendix to check exact field names, required fields, linked object types, and allowed action/state shapes. Linked type names point to another section in this reference. + +### Flow Definition + +Fields: +- `schema` (optional): must be `crewai.flow/v1`; default `crewai.flow/v1`. Declarative Flow schema identifier and version. Include it explicitly in authored declarations. +- `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]`) + +Shape: +- `type: json_schema` + +Fields: +- `type` (optional): must be `json_schema`; default `json_schema`. Inline JSON Schema used as the Flow state contract. +- `json_schema` (required): map of string to any. JSON Schema used to validate and document flow state. Declare required fields with JSON Schema's `required` array. +- `default` (optional): map of string to any | null; default `null`. Default values used to initialize Flow state. Defaults are not the same as schema-required fields. + +### Method (`methods.`) + +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"]}`. +- `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]`) + +Shape: +- `call: crew` + +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 CEL-wrapped values here, for example `${state.topic}` or `${outputs.research_brief}`. 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}"}` + +#### Crew Definition (`methods..do[call=crew].with`) + +Fields: +- `agents` (required): map of string to any | list[map of string to any]. Inline crew agents keyed by agent name. Example: `{"researcher": {"backstory": "Expert at concise technical research.", "goal": "Research {topic}", "role": "Research analyst"}}` +- `tasks` (required): list[any]. Ordered crew tasks. Example: `[{"agent": "researcher", "description": "Research {topic}.", "expected_output": "Key findings about {topic}.", "name": "research_task"}]` +- `inputs` (optional): map of string to any. Static default crew inputs. Values are available to crew agent and task interpolation as `{name}` placeholders, for example `{topic}`. Prefer action-level crew `inputs` for runtime values from `state` or `outputs`, and include placeholders for any inputs the crew must reason over. Example: `{"topic": "AI agents"}` + +#### Crew Agent Definition (`methods..do[call=crew].with.agents.`) + +Fields: +- `role` (required): string. Crew agent role. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research analyst` +- `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"}` + +#### Crew Task Definition (`methods..do[call=crew].with.tasks[]`) + +Fields: +- `description` (required): string. Task instructions. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}.` +- `expected_output` (required): string. Expected task output. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Key findings about {topic}.` +- `name` (optional): string | null; default `null`. Optional task name. Example: `research_task` +- `agent` (optional): string | null; default `null`. Name of the crew agent assigned to this task. Example: `researcher` + +### Agent Action (`methods..do[call=agent]`) + +Shape: +- `call: agent` + +Fields: +- `call` (required): must be `agent`. Action discriminator. Use agent to run an individual inline Agent definition outside of a crew. Example: `agent` +- `with` (required): any. Individual Agent definition to load and execute outside of a crew for this action. Put the agent input in `with.input`; agent actions do not support action-level `inputs`. Example: `{"backstory": "Precise and concise.", "goal": "Answer user questions", "input": "${state.question}", "role": "Analyst", "settings": {"llm": "openai/gpt-4o-mini"}}` + +#### Agent Definition (`methods..do[call=agent].with`) + +Fields: +- `role` (required): string. Individual agent role used by a Flow agent action outside of a crew. Example: `Support specialist` +- `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}` + +### Expression Action (`methods..do[call=expression]`) + +Shape: +- `call: expression` + +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. +- `listen` targets method names and router-emitted event names in one shared namespace. +- A router method result must match one declared `emit` value. +- 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 21487a7a1..973f69068 100644 --- a/lib/cli/tests/test_create_flow.py +++ b/lib/cli/tests/test_create_flow.py @@ -26,6 +26,9 @@ def test_create_flow_declarative_project_can_run( assert pyproject["project"]["requires-python"] assert pyproject["project"]["dependencies"] assert (project_root / pyproject["tool"]["crewai"]["definition"]).is_file() + 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 monkeypatch.chdir(project_root) result = CliRunner().invoke(crewai, ["run"], env={"UV_RUN_RECURSION_DEPTH": "1"}) diff --git a/lib/crewai/src/crewai/flow/flow_definition.py b/lib/crewai/src/crewai/flow/flow_definition.py index 5e445becd..c3f6517eb 100644 --- a/lib/crewai/src/crewai/flow/flow_definition.py +++ b/lib/crewai/src/crewai/flow/flow_definition.py @@ -9,6 +9,7 @@ layer that may have produced it and of the engine that runs it (see from __future__ import annotations +from collections.abc import Sequence import logging from pathlib import Path import re @@ -86,7 +87,7 @@ class FlowDictStateDefinition(BaseModel): ) default: dict[str, Any] | None = Field( default=None, - description="Default state values applied before kickoff inputs.", + description="Default values used to initialize Flow state.", examples=[{"topic": "AI agents", "limit": 3}], ) @@ -121,7 +122,7 @@ class FlowPydanticStateDefinition(BaseModel): ) default: dict[str, Any] | None = Field( default=None, - description="Default state values applied before kickoff inputs.", + description="Default values used to initialize Flow state.", examples=[{"topic": "AI agents", "limit": 3}], ) @@ -148,7 +149,7 @@ class FlowJsonSchemaStateDefinition(BaseModel): ) default: dict[str, Any] | None = Field( default=None, - description="Default state values applied before kickoff inputs.", + description="Default values used to initialize Flow state.", examples=[{"topic": "AI agents", "limit": 3}], ) @@ -160,7 +161,7 @@ class FlowUnknownStateDefinition(BaseModel): type: Literal["unknown"] = Field( default="unknown", - description="Unknown state representation; runtime falls back to dictionary state.", + description="Unknown state representation; execution uses dictionary state.", examples=["unknown"], ) ref: str | None = Field( @@ -170,7 +171,7 @@ class FlowUnknownStateDefinition(BaseModel): ) default: dict[str, Any] | None = Field( default=None, - description="Default state values applied before kickoff inputs.", + description="Default values used to initialize Flow state.", examples=[{"topic": "AI agents", "limit": 3}], ) @@ -189,7 +190,7 @@ class FlowConfigDefinition(BaseModel): tracing: bool | None = Field( default=None, - description="Override for flow tracing; when omitted, runtime defaults apply.", + description="Override for flow tracing; when omitted, execution defaults apply.", examples=[True], ) stream: bool = Field( @@ -204,7 +205,7 @@ class FlowConfigDefinition(BaseModel): ) input_provider: str | None = Field( default=None, - description="Import reference or provider key used to supply flow inputs.", + description="Provider key used to supply initial state.", examples=["my_project.inputs:load_inputs"], ) suppress_flow_events: bool = Field( @@ -383,7 +384,7 @@ class FlowToolActionDefinition(BaseModel): examples=["tool"], ) ref: str = Field( - description="Import reference for a BaseTool class, formatted as module:qualname.", + description="Reference to the CrewAI tool to run.", examples=["my_project.tools:SearchTool"], ) with_: dict[str, ExpressionData] | None = Field( @@ -448,7 +449,8 @@ class FlowCrewActionDefinition(BaseModel): description=( "Input overrides passed to the Crew. String values are evaluated as CEL " "only when the trimmed value starts with ${ and ends with }; all other " - "values are literal." + "values are literal. The resulting values are available to crew agent " + "and task interpolation as `{name}` placeholders." ), examples=[{"topic": "${state.topic}"}], ) @@ -463,7 +465,7 @@ class FlowCrewActionDefinition(BaseModel): class FlowAgentActionDefinition(BaseModel): - """A Flow method action that builds and kicks off a CrewAI agent.""" + """A Flow method action that builds and kicks off one agent outside a crew.""" model_config = ConfigDict( populate_by_name=True, @@ -471,12 +473,18 @@ class FlowAgentActionDefinition(BaseModel): ) call: Literal["agent"] = Field( - description="Action discriminator. Use agent to run an inline Agent definition.", + description=( + "Action discriminator. Use agent to run an individual inline Agent " + "definition outside of a crew." + ), examples=["agent"], ) with_: AgentDefinition = Field( alias="with", - description="Inline Agent definition to load and execute for this action.", + description=( + "Individual Agent definition to load and execute outside of a crew " + "for this action." + ), examples=[ { "role": "Analyst", @@ -515,12 +523,11 @@ class FlowScriptActionDefinition(BaseModel): ) code: str = Field( description=( - "Trusted Python source executed as a generated function. Runtime values are " - "passed as state, outputs, input, and item; they are not interpolated into " - "the source. This is not sandboxed." + "Trusted inline Python source. Values are available as state and outputs; " + "they are not interpolated into the source. This is not sandboxed." ), examples=[ - "state['normalized_topic'] = input.strip()\n" + "state['normalized_topic'] = state['topic'].strip()\n" "return state['normalized_topic']" ], ) @@ -645,13 +652,13 @@ class FlowMethodDefinition(BaseModel): ) do: FlowActionDefinition = Field( description="Action executed when this method runs.", - examples=[{"call": "script", "code": "return input.strip()"}], + examples=[{"call": "expression", "expr": "state.topic"}], ) start: bool | FlowDefinitionCondition | None = Field( default=None, description=( "Marks a start method. True starts unconditionally; a condition starts " - "when the kickoff inputs or events satisfy it." + "when the initial state or events satisfy it." ), examples=[True], ) @@ -729,12 +736,12 @@ class FlowDefinition(BaseModel): ) state: FlowStateDefinition | None = Field( default=None, - description="State contract for kickoff inputs and runtime state.", + description="State contract for the initial state and updates during execution.", examples=[{"type": "dict", "default": {"topic": "AI agents"}}], ) config: FlowConfigDefinition = Field( default_factory=FlowConfigDefinition, - description="Serializable flow-level runtime configuration.", + description="Serializable flow-level execution configuration.", examples=[{"stream": True, "max_method_calls": 20}], ) persist: FlowPersistenceDefinition | None = Field( @@ -835,6 +842,18 @@ class FlowDefinition(BaseModel): log_flow_definition_issues(definition) return definition + @classmethod + def skill( + cls, + *, + skips: Sequence[str] = (), + examples_format: Literal["yaml", "json"] = "yaml", + ) -> str: + """Return a portable Markdown skill for authoring Flow declarations.""" + from crewai.flow.skill import render_skill_markdown + + return render_skill_markdown(skips=skips, examples_format=examples_format) + def _validate_step_name(name: str, *, field: str) -> None: if not isinstance(name, str) or not _STEP_NAME_PATTERN.fullmatch(name): diff --git a/lib/crewai/src/crewai/flow/skill.py b/lib/crewai/src/crewai/flow/skill.py new file mode 100644 index 000000000..e6c1b1339 --- /dev/null +++ b/lib/crewai/src/crewai/flow/skill.py @@ -0,0 +1,551 @@ +"""Markdown skill rendering for Flow Definition authoring.""" + +from collections.abc import Sequence +from dataclasses import dataclass, field +import json +from pathlib import Path +import re +from typing import Any, Literal + +from jinja2 import Environment, FileSystemLoader +import yaml + +from crewai.flow.flow_definition import FlowDefinition + + +SKIP_BY_MODEL: dict[str, str] = { + "FlowScriptActionDefinition": "script_action", + "FlowToolActionDefinition": "tool_action", + "FlowExpressionActionDefinition": "expression_action", + "FlowEachActionDefinition": "each", + "FlowEachStepDefinition": "each", + "FlowConfigDefinition": "config", + "FlowHumanFeedbackDefinition": "hitl", + "FlowPersistenceDefinition": "persistence", +} + +FIELD_TYPE_OVERRIDES: dict[tuple[str, str], str] = { + ("FlowDefinition", "state"): "[State](#json-schema-state-statetypejson_schema)", + ("FlowDefinition", "methods"): "map of string to [Method](#method-methods)", + ("FlowMethodDefinition", "do"): "[Action](#action)", + ("FlowCrewActionDefinition", "with"): "inline crew definition", +} + +_TEMPLATES_DIR = Path(__file__).parent / "templates" +_ENVIRONMENT = Environment( # noqa: S701 - renders trusted Markdown, not HTML. + loader=FileSystemLoader(_TEMPLATES_DIR), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=False, +) + + +def render_skill_markdown( + *, + skips: Sequence[str] = (), + examples_format: Literal["yaml", "json"] = "yaml", +) -> str: + if examples_format not in ("yaml", "json"): + raise ValueError("Flow skill examples_format must be 'yaml' or 'json'") + + skips_set = frozenset(skips) + rendered = _ENVIRONMENT.get_template("flow_definition_skill.md.j2").render( + template_context(skips_set, examples_format) + ) + rendered = re.sub(r"\n{3,}", "\n\n", rendered) + return rendered.strip() + "\n" + + +def template_context( + skips: frozenset[str], examples_format: Literal["yaml", "json"] = "yaml" +) -> dict[str, Any]: + return { + "examples_format": examples_format, + "example": render_flow_example(examples_format), + "example_language": examples_format, + "include_each_action": "each" not in skips, + "include_conversational": "conversational" not in skips, + "include_hitl": "hitl" not in skips, + "include_non_linear_flows": "non_linear_flows" not in skips, + "include_persistence": "persistence" not in skips, + "include_expression_action": "expression_action" not in skips, + "include_script_action": "script_action" not in skips, + "include_tool_action": "tool_action" not in skips, + "sections": FlowSkillReferenceExtractor(skips=skips).extract(), + } + + +def render_flow_example(examples_format: Literal["yaml", "json"]) -> str: + example_yaml = (_TEMPLATES_DIR / "flow_definition_example.yaml").read_text( + encoding="utf-8" + ) + if examples_format == "json": + return json.dumps(yaml.safe_load(example_yaml), indent=2) + return example_yaml.rstrip() + + +@dataclass(frozen=True) +class ModelSpec: + name: str + section: str + address: str = "" + label: str = "" + hidden: bool = False + examples: bool = False + descriptions: dict[str, str] = field(default_factory=dict) + + @property + def display_title(self) -> str: + return self.label or MODEL_TITLES.get(self.name, self.section) + + @property + def display_label(self) -> str: + if not self.address: + return self.display_title + return f"{self.display_title} (`{self.address}`)" + + +MODEL_TITLES = { + "FlowDefinition": "Flow Definition", + "FlowDictStateDefinition": "Dict State", + "FlowPydanticStateDefinition": "Pydantic State", + "FlowJsonSchemaStateDefinition": "JSON Schema State", + "FlowUnknownStateDefinition": "Unknown State", + "FlowMethodDefinition": "Method", + "FlowCodeActionDefinition": "Code Action", + "FlowScriptActionDefinition": "Script Action", + "FlowToolActionDefinition": "Tool Action", + "FlowCrewActionDefinition": "Crew Action", + "FlowAgentActionDefinition": "Agent Action", + "FlowExpressionActionDefinition": "Expression Action", + "FlowEachActionDefinition": "Each Action", + "FlowEachStepDefinition": "Each Step", + "CrewDefinition": "Crew Definition", + "CrewAgentDefinition": "Crew Agent Definition", + "CrewTaskDefinition": "Crew Task Definition", + "AgentDefinition": "Agent Definition", + "FlowConfigDefinition": "Config", + "FlowPersistenceDefinition": "Persistence", + "FlowHumanFeedbackDefinition": "Human Feedback", +} + + +MODEL_SPECS: tuple[ModelSpec, ...] = ( + ModelSpec( + "FlowDefinition", + "Flow Definition", + descriptions={ + "schema": "Declarative Flow schema identifier and version. Include it explicitly in authored declarations.", + "conversational": "Top-level conversational flow configuration, only when the flow supports chat.", + }, + ), + ModelSpec("FlowDictStateDefinition", "State", "state[type=dict]", hidden=True), + ModelSpec( + "FlowPydanticStateDefinition", "State", "state[type=pydantic]", hidden=True + ), + ModelSpec( + "FlowJsonSchemaStateDefinition", + "State", + "state[type=json_schema]", + descriptions={ + "json_schema": "JSON Schema used to validate and document flow state. Declare required fields with JSON Schema's `required` array.", + "default": "Default values used to initialize Flow state. Defaults are not the same as schema-required fields.", + }, + ), + ModelSpec( + "FlowUnknownStateDefinition", "State", "state[type=unknown]", hidden=True + ), + ModelSpec( + "FlowMethodDefinition", + "Method", + "methods.", + descriptions={ + "do": "Single action object executed when this method runs.", + "start": "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": '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"]}`.', + "router": "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": "Declared router events this method may emit. Each emitted event name should be unique and should not collide with method names.", + }, + ), + ModelSpec( + "FlowCodeActionDefinition", + "Action", + "methods..do[call=code]", + hidden=True, + ), + ModelSpec("FlowScriptActionDefinition", "Action", "methods..do[call=script]"), + ModelSpec("FlowToolActionDefinition", "Action", "methods..do[call=tool]"), + ModelSpec( + "FlowCrewActionDefinition", + "Action", + "methods..do[call=crew]", + examples=True, + descriptions={ + "call": "Action discriminator. Use crew to run an inline Crew definition.", + "inputs": "Actual kickoff inputs passed to the Crew. Use CEL-wrapped values here, for example `${state.topic}` or `${outputs.research_brief}`. 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.", + }, + ), + ModelSpec( + "FlowAgentActionDefinition", + "Action", + "methods..do[call=agent]", + examples=True, + descriptions={ + "with": "Individual Agent definition to load and execute outside of a crew for this action. Put the agent input in `with.input`; agent actions do not support action-level `inputs`.", + }, + ), + ModelSpec( + "FlowExpressionActionDefinition", + "Action", + "methods..do[call=expression]", + ), + ModelSpec("FlowEachActionDefinition", "Action", "methods..do[call=each]"), + ModelSpec( + "FlowEachStepDefinition", + "Each Step", + "methods..do[call=each].do[]", + ), + ModelSpec( + "CrewDefinition", + "Crew Definition", + "methods..do[call=crew].with", + hidden=True, + examples=True, + descriptions={ + "inputs": "Static default crew inputs. Values are available to crew agent and task interpolation as `{name}` placeholders, for example `{topic}`. Prefer action-level crew `inputs` for runtime values from `state` or `outputs`, and include placeholders for any inputs the crew must reason over.", + "manager_agent": "Optional manager agent name.", + }, + ), + ModelSpec( + "CrewAgentDefinition", + "Crew Agent Definition", + "methods..do[call=crew].with.agents.", + hidden=True, + examples=True, + ), + ModelSpec( + "CrewTaskDefinition", + "Crew Task Definition", + "methods..do[call=crew].with.tasks[]", + hidden=True, + examples=True, + descriptions={ + "name": "Optional task name.", + }, + ), + ModelSpec( + "AgentDefinition", + "Agent Definition", + "methods..do[call=agent].with", + 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.", + }, + ), + ModelSpec("FlowConfigDefinition", "Config", "config"), + ModelSpec("FlowPersistenceDefinition", "Persistence", "persist"), + ModelSpec( + "FlowHumanFeedbackDefinition", + "Human Feedback", + "methods..human_feedback", + ), +) + +_SPECS_BY_NAME: dict[str, ModelSpec] = {spec.name: spec for spec in MODEL_SPECS} + + +@dataclass(frozen=True) +class FlowSkillReferenceExtractor: + skips: frozenset[str] + schema: dict[str, Any] = field( + default_factory=lambda: FlowDefinition.model_json_schema(by_alias=True) + ) + + def extract(self) -> list[dict[str, Any]]: + sections: list[dict[str, Any]] = [] + + for spec in MODEL_SPECS: + if spec.hidden or self.model_is_skipped(spec.name): + continue + + if not sections or sections[-1]["label"] != spec.section: + sections.append( + { + "label": spec.section, + "models": [], + "kind": "union" if spec.section == "Action" else "object", + } + ) + sections[-1]["models"].append(self.extract_model(spec)) + + for section in sections: + if section["kind"] != "union" and section["models"]: + section["label"] = section["models"][0]["label"] + + return sections + + def extract_model(self, spec: ModelSpec) -> dict[str, Any]: + model_name = spec.name + model_schema = ( + self.schema + if model_name == "FlowDefinition" + else self.schema["$defs"][model_name] + ) + required_from_schema = set(model_schema.get("required", ())) + fields = [] + + for field_name, field_schema in model_schema.get("properties", {}).items(): + if self.field_is_hidden(model_name, field_name): + continue + + required = ( + field_name in required_from_schema + or ( + model_name == "FlowDefinition" + and field_name in ("state", "methods") + ) + or (model_name == "FlowCrewActionDefinition" and field_name == "with") + ) + fields.append( + { + "name": field_name, + "type": self.render_field_type( + model_name, field_name, field_schema + ), + "required": required, + "default": render_field_default( + model_name, field_name, field_schema, required + ), + "description": self.render_field_description( + spec, model_name, field_name, field_schema + ), + "examples": render_field_examples(spec, field_name, field_schema), + } + ) + + return { + "label": spec.display_label, + "anchor": f"#{markdown_heading_anchor(spec.display_label)}", + "link": ( + f"[{spec.display_label}](#{markdown_heading_anchor(spec.display_label)})" + ), + "discriminator": extract_discriminator(model_schema), + "fields": fields, + "inline_models": self.inline_models_for(model_name), + } + + def inline_models_for(self, model_name: str) -> list[dict[str, Any]]: + names_by_model = { + "FlowCrewActionDefinition": ( + "CrewDefinition", + "CrewAgentDefinition", + "CrewTaskDefinition", + ), + "FlowAgentActionDefinition": ("AgentDefinition",), + } + return [ + self.extract_model(_SPECS_BY_NAME[name]) + for name in names_by_model.get(model_name, ()) + ] + + def model_is_skipped(self, model_name: str) -> bool: + skip = SKIP_BY_MODEL.get(model_name) + return skip in self.skips if skip is not None else False + + def field_is_hidden( + self, + model_name: str, + field_name: str, + ) -> bool: + return ( + ("hitl" in self.skips and field_name == "human_feedback") + or ("persistence" in self.skips and field_name == "persist") + or ("config" in self.skips and field_name == "config") + or ("conversational" in self.skips and field_name == "conversational") + or (model_name == "AgentDefinition" and field_name == "response_format") + or (model_name == "CrewDefinition" and field_name == "manager_agent") + or (model_name == "CrewTaskDefinition" and field_name == "context") + or ( + field_name == "type" + and model_name + in {"AgentDefinition", "CrewAgentDefinition", "CrewTaskDefinition"} + ) + or (field_name == "ref" and model_name != "FlowToolActionDefinition") + or ( + model_name == "FlowCrewActionDefinition" + and field_name == "from_declaration" + ) + ) + + def render_field_type( + self, + model_name: str, + field_name: str, + field_schema: dict[str, Any], + ) -> str: + if override := FIELD_TYPE_OVERRIDES.get((model_name, field_name)): + return override + return self.render_schema_type(field_schema) or "any" + + def render_schema_type(self, field_schema: dict[str, Any]) -> str | None: + if "$ref" in field_schema: + return self.render_schema_ref(field_schema["$ref"]) + if "const" in field_schema: + return f"must be {format_inline_value(field_schema['const'])}" + if "enum" in field_schema: + values = ", ".join( + format_inline_value(value) for value in field_schema["enum"] + ) + return f"one of {values}" + + for union_key in ("anyOf", "oneOf", "allOf"): + if union_key in field_schema: + return join_unique( + self.render_schema_type(option) + for option in field_schema[union_key] + ) + + json_type = field_schema.get("type") + if isinstance(json_type, list): + return join_unique( + self.render_schema_type({"type": item}) for item in json_type + ) + if json_type == "array": + item_type = self.render_schema_type(field_schema.get("items", {})) or "any" + return ( + f"list of {item_type}" + if item_type.startswith("[") + else f"list[{item_type}]" + ) + if json_type == "object": + additional_properties = field_schema.get("additionalProperties") + if isinstance(additional_properties, dict): + value_type = self.render_schema_type(additional_properties) or "any" + return f"map of string to {value_type}" + return "map of string to any" if additional_properties is True else "object" + if isinstance(json_type, str): + return json_type + return "object" if "properties" in field_schema else "any" + + def render_schema_ref(self, ref: str) -> str | None: + schema_name = ref.rsplit("/", 1)[-1] + if schema_name == "ExpressionData": + return ( + "expression data" + if "expression_action" not in self.skips + else "dynamic value" + ) + if schema_name == "PythonReferenceDefinition": + return None + spec = _SPECS_BY_NAME.get(schema_name) + if (spec and spec.hidden) or self.model_is_skipped(schema_name): + return None + if spec is None: + return "object" + return f"[{spec.display_label}](#{markdown_heading_anchor(spec.display_label)})" + + def render_field_description( + self, + spec: ModelSpec, + model_name: str, + field_name: str, + field_schema: dict[str, Any], + ) -> str | None: + if "non_linear_flows" in self.skips and model_name == "FlowMethodDefinition": + if field_name == "start": + return "Marks the single normal entrypoint. Use `true`." + if field_name == "listen": + return "Runs this method after one upstream method or router-emitted event." + return render_field_description(spec, field_name, field_schema) + + +def render_field_default( + model_name: str, + field_name: str, + field_schema: dict[str, Any], + required: bool, +) -> str | None: + if required: + return None + if model_name == "FlowDefinition" and field_name == "config": + return "generated default" + if "default" in field_schema: + return format_inline_value(field_schema["default"]) + return None + + +def extract_discriminator(model_schema: dict[str, Any]) -> dict[str, str] | None: + properties = model_schema.get("properties", {}) + for field_name in ("call", "type"): + if field_name not in properties: + continue + value = properties[field_name].get( + "const", properties[field_name].get("default") + ) + if value is not None: + return {"name": field_name, "value": str(value)} + return None + + +def join_unique(values: Any) -> str | None: + rendered_values = list( + dict.fromkeys(value for value in values if value is not None) + ) + return " | ".join(rendered_values) or None + + +def markdown_heading_anchor(text: str) -> str: + heading = re.sub(r"<[^>]+>", "", text) + heading = re.sub(r"`([^`]*)`", r"\1", heading) + heading = heading.lower() + heading = re.sub(r"[^\w\s-]", "", heading) + return re.sub(r"\s+", "-", heading.strip()) + + +def format_inline_value(value: Any) -> str: + if value is None: + return "`null`" + if isinstance(value, bool): + return f"`{str(value).lower()}`" + return f"`{value}`" + + +def render_field_description( + spec: ModelSpec, field_name: str, field_schema: dict[str, Any] +) -> str | None: + if field_name in spec.descriptions: + return spec.descriptions[field_name] + return field_schema.get("description") + + +def render_field_examples( + spec: ModelSpec, field_name: str, field_schema: dict[str, Any] +) -> list[str]: + if not spec.examples: + return [] + + examples = ( + example + for example in field_schema.get("examples", ()) + if not contains_python_reference(example) + ) + return [format_inline_example(example) for example in examples] + + +def contains_python_reference(value: Any) -> bool: + if isinstance(value, dict): + return "python" in value or any( + contains_python_reference(item) for item in value.values() + ) + if isinstance(value, list): + return any(contains_python_reference(item) for item in value) + return False + + +def format_inline_example(value: Any) -> str: + if isinstance(value, str): + return format_inline_value(value.replace("\n", "\\n")) + if value is None or isinstance(value, (bool, int, float)): + return format_inline_value(value) + return f"`{json.dumps(value, ensure_ascii=True)}`" diff --git a/lib/crewai/src/crewai/flow/templates/flow_definition_example.yaml b/lib/crewai/src/crewai/flow/templates/flow_definition_example.yaml new file mode 100644 index 000000000..b0d7a8ed1 --- /dev/null +++ b/lib/crewai/src/crewai/flow/templates/flow_definition_example.yaml @@ -0,0 +1,69 @@ +schema: crewai.flow/v1 +name: ResearchReviewFlow +state: + type: json_schema + json_schema: + type: object + properties: + topic: + type: string + audience: + type: string + required: + - topic + - audience + default: + topic: AI agent orchestration + audience: platform engineering leaders +methods: + research_brief: + start: true + do: + call: crew + with: + agents: + researcher: + role: Research analyst + goal: Research {topic} for {audience} + backstory: Expert at concise technical research. + reviewer: + role: Strategy reviewer + goal: Decide whether the research needs an executive follow-up + backstory: Experienced at reviewing technical briefs for leaders. + tasks: + - name: research_task + description: Research {topic} for {audience}. + expected_output: Key findings and tradeoffs. + agent: researcher + - name: review_task + description: Review the research and decide if an executive follow-up is needed. + expected_output: 'A brief review ending with `needs_followup: true` or `needs_followup: false`.' + agent: reviewer + inputs: + topic: Default topic + audience: Default audience + inputs: + topic: "${state.topic}" + audience: "${state.audience}" + route_followup: + listen: research_brief + router: true + emit: + - followup + - done + do: + call: agent + with: + role: Follow-up router + goal: 'Return exactly one bare value: followup or done. Do not include explanation.' + backstory: Skilled at routing reviewed research briefs. + input: "${outputs.research_brief.raw}" + write_followup: + listen: followup + do: + call: agent + with: + role: Executive communications specialist + goal: Draft a concise executive follow-up from the reviewed research + backstory: Writes crisp follow-ups for technical leaders. + input: "${outputs.research_brief.raw}" 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 new file mode 100644 index 000000000..0db600d22 --- /dev/null +++ b/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 @@ -0,0 +1,203 @@ +--- +name: flow-definition +description: Create or edit CrewAI Flow declarations. Use when the user needs a YAML or JSON flow with methods, state, agents, crews, tools, outputs, or conditional branches. +--- + +# Flow Definition + +You are writing a CrewAI Flow declaration for the user. +Use these instructions when the user asks you to create or edit a Flow. +Return one valid `crewai.flow/v1` YAML or JSON document. + +Treat this document as instructions for you, not as text to show the user. +Follow the examples for shape and formatting, then use the API reference to check exact fields. + +## Output Format + +Return one valid `crewai.flow/v1` Flow declaration. +Do not include explanatory prose unless the user asks for it. + +## Build It In This Order + +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. +{% if include_non_linear_flows %} +3. Add at least one method with `start: true`. +{% else %} +3. Add exactly one method with `start: true`. +{% endif %} +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`. +7. Before final output, check every `listen`, `emit`, and `outputs.some_method` reference. + +Method names must match `^[A-Za-z_][A-Za-z0-9_]*$`. + +## Choose One Action Per Method + +Pick the simplest action that does the job. + +{% if include_expression_action %} +- Use `call: expression` for simple reads, filters, computed values, and deterministic routing. +{% endif %} +{% if include_tool_action %} +- 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. +- 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`. +{% endif %} +{% if include_hitl %} +- Use `human_feedback` when a method needs a human checkpoint. +{% endif %} +{% if include_script_action %} +- Use `call: script` only for trusted inline Python. Scripts are not sandboxed. +{% endif %} + +## Wire Methods Explicitly + +- `state` is the initial shared data shape. Action results do not automatically merge into `state`. +- Read method results with `outputs.method_name` after that method can run. +- `listen` targets a method name or a router-emitted event name. +- 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. +{% if include_non_linear_flows %} +- 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. +{% else %} +- Use `start: true` for the single entrypoint. +{% endif %} + +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.` +{% if include_expression_action %} +Prefer `call: expression` when routing can be computed without an agent. +{% endif %} + +## CEL And Dynamic Values + +CEL is the expression language for reading Flow data and making small decisions. +Use {% if include_tool_action %}tools, {% endif %}agents and crews{% if include_script_action %}, or trusted scripts{% endif %} for larger work or side effects. + +Use these expression forms correctly: + +{% if include_expression_action or include_each_action %} +- Raw CEL: use in {% if include_expression_action and include_each_action %}`expr`, `in`, and `if`{% elif include_expression_action %}`expr`{% else %}`in` and `if`{% endif %}. Do not wrap raw CEL in `${...}`. +{% endif %} +- 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. +- 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`. + +Available CEL variables: + +- `state`: initial input data, for example `state.ticket.subject`. +- `outputs`: completed method outputs, for example `outputs.classify_ticket`. +{% if include_each_action %} +- `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`. +{% endif %} + +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}"`. +- 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`. +- Crew action-level `inputs` alone are not grounding. Include placeholders for the facts the model must use. +- Crew outputs are objects. Use `${outputs.research_brief.raw}` for text. +- For structured crew output, use fields like `${outputs.research_brief.json_dict.field}` or `${outputs.research_brief.pydantic.field}`. +- Do not pass a whole crew output to an agent input, like `${outputs.research_brief}`. +- 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. +{% if include_script_action %} +- Trusted `script` actions can mutate state explicitly. Use them only when the user asks for that behavior. +{% endif %} + +## Do Not + +- Do not invent top-level keys outside the Flow declaration shape. +- 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 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`{% if include_hitl %} or `human_feedback.emit`{% endif %}. +- 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. +{% if include_conversational %} +- 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. +{% endif %} +{% if include_each_action %} +- Do not use `each` without at least one named step. +{% endif %} +{% if include_script_action %} +- Do not use `script` for untrusted input or user-authored code. +{% endif %} + +## Examples + +### Crew review with routed follow-up + +```{{ example_language }} +{{ example }} +``` + +## API Reference + +Use this appendix to check exact field names, required fields, linked object types, and allowed action/state shapes. Linked type names point to another section in this reference. +{% macro render_model(model) -%} +{% if model.discriminator %} + +Shape: +- `{{ model.discriminator.name }}: {{ model.discriminator.value }}` +{% endif %} + +Fields: +{% for field in model.fields %} +- `{{ field.name }}` ({% if field.required %}required{% else %}optional{% endif %}): {{ field.type }}{% if field.default %}; default {{ field.default }}{% endif %}{% if field.description %}. {{ field.description }}{% endif %}{% if field.examples %}{% if not field.description %}.{% endif %} Example{% if field.examples|length > 1 %}s{% endif %}: {{ field.examples | join(", ") }}{% endif +%} +{% endfor %} +{% for inline_model in model.inline_models %} + +#### {{ inline_model.label }} +{{ render_model(inline_model) }} +{% endfor %} +{%- endmacro %} +{% for section in sections %} + +### {{ section.label }} +{% if section.kind == "union" %} + +Discriminated union by `{% if section.label == "State" %}type{% else %}call{% endif %}`. + +Allowed shapes: +{% for model in section.models %} +- [`{{ model.discriminator.name }}: {{ model.discriminator.value }}`]({{ model.anchor }}) +{% endfor %} +{% for model in section.models %} + +### {{ model.label }} +{{ render_model(model) }} +{% endfor %} +{% else %} +{{ render_model(section.models[0]) }} +{% endif %} + +{% endfor %} +### Cross-Field Rules + +- A method has exactly one `do` action object with one `call` discriminator. +- `listen` targets method names and router-emitted event names in one shared namespace. +- A router method result must match one declared `emit` value. +- 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}`. +{% if include_each_action %} +- `each.do` must contain at least one named step. +{% endif %} diff --git a/lib/crewai/src/crewai/project/crew_definition.py b/lib/crewai/src/crewai/project/crew_definition.py index 9e88c2ad6..863e97086 100644 --- a/lib/crewai/src/crewai/project/crew_definition.py +++ b/lib/crewai/src/crewai/project/crew_definition.py @@ -19,7 +19,10 @@ __all__ = [ class PythonReferenceDefinition(BaseModel): """Dotted Python reference used by crew definitions.""" - python: str + python: str = Field( + description="Dotted Python import path to load.", + examples=["my_project.schemas.SupportReply"], + ) @field_validator("python") @classmethod @@ -40,11 +43,37 @@ class CrewAgentDefinition(BaseModel): model_config = ConfigDict(extra="allow") - role: str - goal: str - backstory: str - type: str | PythonReferenceDefinition | None = None - settings: dict[str, Any] = Field(default_factory=dict) + role: str = Field( + description=( + "Crew agent role. Crew inputs are interpolated with `{name}` " + "placeholders such as `{topic}`; this is not CEL." + ), + examples=["Research analyst"], + ) + goal: str = Field( + description=( + "Crew agent goal. Crew inputs are interpolated with `{name}` " + "placeholders such as `{topic}`; this is not CEL." + ), + examples=["Research {topic}"], + ) + backstory: str = Field( + description=( + "Crew agent backstory. Crew inputs are interpolated with `{name}` " + "placeholders such as `{topic}`; this is not CEL." + ), + examples=["Expert at concise technical research."], + ) + type: str | PythonReferenceDefinition | None = Field( + default=None, + description="Optional built-in type or Python reference used to load the agent.", + examples=["agent", {"python": "my_project.agents.ResearchAgent"}], + ) + settings: dict[str, Any] = Field( + default_factory=dict, + description="Additional agent settings passed to the loader.", + examples=[{"llm": "openai/gpt-4o-mini"}], + ) @field_validator("settings", mode="before") @classmethod @@ -55,10 +84,41 @@ class CrewAgentDefinition(BaseModel): class AgentDefinition(CrewAgentDefinition): - """Inline agent definition used by a Flow agent action.""" + """Inline individual agent definition used outside of a crew.""" - input: str - response_format: PythonReferenceDefinition | None = None + role: str = Field( + description="Individual agent role used by a Flow agent action outside of a crew.", + examples=["Support specialist"], + ) + goal: str = Field( + description="Individual agent goal for the Flow agent action outside of a crew.", + examples=["Draft a concise customer reply"], + ) + backstory: str = Field( + description=( + "Individual agent backstory used to shape behavior outside of a crew." + ), + examples=["Expert at resolving SaaS support questions."], + ) + type: str | PythonReferenceDefinition | None = Field( + default=None, + description="Optional built-in type or Python reference used to load the agent.", + examples=["agent", {"python": "my_project.agents.SupportAgent"}], + ) + settings: dict[str, Any] = Field( + default_factory=dict, + description="Additional agent settings passed to the loader.", + examples=[{"llm": "openai/gpt-4o-mini"}], + ) + input: str = Field( + description="Input passed to the individual agent kickoff outside of a crew.", + examples=["${state.ticket.body}"], + ) + response_format: PythonReferenceDefinition | None = Field( + default=None, + description="Optional Python reference to a Pydantic response format.", + examples=[{"python": "my_project.schemas.SupportReply"}], + ) @field_validator("input", mode="before") @classmethod @@ -73,12 +133,40 @@ class CrewTaskDefinition(BaseModel): model_config = ConfigDict(extra="allow") - description: str - expected_output: str - name: str | None = None - agent: str | None = None - context: list[str] | None = None - type: str | PythonReferenceDefinition | None = None + description: str = Field( + description=( + "Task instructions. Crew inputs are interpolated with `{name}` " + "placeholders such as `{topic}`; this is not CEL." + ), + examples=["Research {topic}."], + ) + expected_output: str = Field( + description=( + "Expected task output. Crew inputs are interpolated with `{name}` " + "placeholders such as `{topic}`; this is not CEL." + ), + examples=["Key findings about {topic}."], + ) + name: str | None = Field( + default=None, + description="Optional task name used by context references.", + examples=["research_task"], + ) + agent: str | None = Field( + default=None, + description="Name of the crew agent assigned to this task.", + examples=["researcher"], + ) + context: list[str] | None = Field( + default=None, + description="Names of previous tasks whose outputs should be used as context.", + examples=[["research_task"]], + ) + type: str | PythonReferenceDefinition | None = Field( + default=None, + description="Optional built-in type or Python reference used to load the task.", + examples=["task", {"python": "my_project.tasks.ResearchTask"}], + ) _CrewAgentsInput: TypeAlias = dict[str, CrewAgentDefinition] | list[dict[str, Any]] @@ -89,10 +177,44 @@ class CrewDefinition(BaseModel): model_config = ConfigDict(extra="allow") - agents: dict[str, CrewAgentDefinition] - tasks: list[CrewTaskDefinition] - inputs: dict[str, Any] = Field(default_factory=dict) - manager_agent: str | PythonReferenceDefinition | None = None + agents: dict[str, CrewAgentDefinition] = Field( + description="Inline crew agents keyed by agent name.", + examples=[ + { + "researcher": { + "role": "Research analyst", + "goal": "Research {topic}", + "backstory": "Expert at concise technical research.", + } + } + ], + ) + tasks: list[CrewTaskDefinition] = Field( + description="Ordered crew tasks.", + examples=[ + [ + { + "name": "research_task", + "description": "Research {topic}.", + "expected_output": "Key findings about {topic}.", + "agent": "researcher", + } + ] + ], + ) + inputs: dict[str, Any] = Field( + default_factory=dict, + description=( + "Default crew inputs. Values are available to crew agent and task " + "interpolation as `{name}` placeholders, for example `{topic}`." + ), + examples=[{"topic": "AI agents"}], + ) + manager_agent: str | PythonReferenceDefinition | None = Field( + default=None, + description="Optional manager agent name or Python reference.", + examples=["manager", {"python": "my_project.agents.ManagerAgent"}], + ) @field_validator("inputs", mode="before") @classmethod diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index 191bdea62..df1453e3b 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -82,8 +82,9 @@ def test_flow_definition_json_schema_carries_reference_descriptions(): assert "not sandboxed" in script_properties["code"]["description"] agent_properties = defs["FlowAgentActionDefinition"]["properties"] - assert "Inline Agent definition" in agent_properties["with"]["description"] - assert "run an inline Agent" in agent_properties["call"]["description"] + assert "Individual Agent definition" in agent_properties["with"]["description"] + assert "outside of a crew" in agent_properties["with"]["description"] + assert "individual inline Agent" in agent_properties["call"]["description"] state_schema = next( branch @@ -154,7 +155,7 @@ def test_flow_definition_json_schema_carries_field_examples_only(): script_properties = defs["FlowScriptActionDefinition"]["properties"] assert script_properties["call"]["examples"] == ["script"] - assert "input.strip()" in script_properties["code"]["examples"][0] + assert "state['topic'].strip()" in script_properties["code"]["examples"][0] assert script_properties["language"]["examples"] == ["python"] action_properties = defs["FlowCodeActionDefinition"]["properties"] @@ -1300,3 +1301,46 @@ def test_flow_definition_allows_router_without_trigger(caplog): StandaloneRouterFlow.flow_definition() assert not caplog.records + + +def test_skill_documents_flow_wiring(): + skill = flow_definition.FlowDefinition.skill() + + assert isinstance(skill, str) + assert "```yaml" in skill + assert "[Method](#method-methods)" in skill + + +def test_skill_can_render_json_examples(): + skill = flow_definition.FlowDefinition.skill(examples_format="json") + + assert "```json" in skill + assert '"schema": "crewai.flow/v1"' in skill + assert "```yaml" not in skill + + +def test_skill_ignores_unknown_skips(): + skill = flow_definition.FlowDefinition.skill(skips=["unknown"]) + + assert "[Method](#method-methods)" in skill + + +def test_skill_with_skips_is_shorter(): + full = flow_definition.FlowDefinition.skill() + trimmed = flow_definition.FlowDefinition.skill( + skips=[ + "each", + "hitl", + "persistence", + "expression_action", + "script_action", + "tool_action", + ] + ) + + assert "[Method](#method-methods)" in trimmed + assert "call: expression" not in trimmed + assert "Prefer `call: expression`" not in trimmed + assert "call: script" not in trimmed + assert "call: tool" not in trimmed + assert len(trimmed) < len(full)