From 1556dbea3ef7d2d4f6cdb4f0825596ea46be547f Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Tue, 30 Jun 2026 09:13:33 -0700 Subject: [PATCH] Add generated Flow Definition authoring skill (#6393) * Add generated Flow Definition authoring skill Generate a portable skill from the Flow Definition schema so agents can author valid declarative flows with the same reference CrewAI uses to validate them. New declarative flow projects now write this skill. ```python from crewai.flow.flow_definition import FlowDefinition skill = FlowDefinition.skill(skips=(), examples_format="yaml") ``` * `examples_format` accepts `"yaml"` or `"json"`. * Supported skips: `conversational`, `non_linear_flows`, `each`, `hitl`, `persistence`, `config`, `expression_action`, `script_action`, `tool_action` The generated skill includes authoring rules, a routed crew example, and an API reference extracted from the Flow, action, state, agent, crew, and task Pydantic schemas. * Fix declarative flow scaffold without framework import * Fix skipped expression action guidance * Fix markdown links in skill --- lib/cli/src/crewai_cli/create_flow.py | 7 +- .../templates/declarative_flow/AGENTS.md | 375 ++++++++++++ lib/cli/tests/test_create_flow.py | 3 + lib/crewai/src/crewai/flow/flow_definition.py | 59 +- lib/crewai/src/crewai/flow/skill.py | 551 ++++++++++++++++++ .../templates/flow_definition_example.yaml | 69 +++ .../templates/flow_definition_skill.md.j2 | 203 +++++++ .../src/crewai/project/crew_definition.py | 160 ++++- lib/crewai/tests/test_flow_definition.py | 50 +- 9 files changed, 1430 insertions(+), 47 deletions(-) create mode 100644 lib/cli/src/crewai_cli/templates/declarative_flow/AGENTS.md create mode 100644 lib/crewai/src/crewai/flow/skill.py create mode 100644 lib/crewai/src/crewai/flow/templates/flow_definition_example.yaml create mode 100644 lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 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)