Add declarative Flow CLI support (#6294)

* Add declarative Flow CLI support

Currently, declarative flows can be loaded by the runtime, but the CLI
still treats them as an experimental definition file instead of a
first-class Flow project shape.

With this PR, `crewai create flow --declarative` scaffolds a YAML-backed
Flow project, and `crewai run`, `crewai flow kickoff`, and `crewai flow
plot` can run against the configured definition.

This also lets crew actions reference reusable crew definition files or
folders and override their inputs from the Flow definition, so
declarative flows can compose existing declarative crews without
inlining everything.

* Address code review comments
This commit is contained in:
Vinicius Brasil
2026-06-22 19:58:17 -07:00
committed by GitHub
parent 0391febc6c
commit 4b2ce00a09
18 changed files with 951 additions and 334 deletions

View File

@@ -40,12 +40,12 @@ def replay_task_command(*args: Any, **kwargs: Any) -> Any:
return _replay_task_command(*args, **kwargs)
def run_flow_definition(*args: Any, **kwargs: Any) -> Any:
from crewai_cli.run_flow_definition import (
run_flow_definition as _run_flow_definition,
def run_declarative_flow(*args: Any, **kwargs: Any) -> Any:
from crewai_cli.run_declarative_flow import (
run_declarative_flow as _run_declarative_flow,
)
return _run_flow_definition(*args, **kwargs)
return _run_declarative_flow(*args, **kwargs)
def run_crew(*args: Any, **kwargs: Any) -> Any:
@@ -155,12 +155,18 @@ def uv(uv_args: tuple[str, ...]) -> None:
is_flag=True,
help="Use classic Python/YAML project structure instead of JSON",
)
@click.option(
"--declarative",
is_flag=True,
help="Create a declarative Flow project instead of a Python Flow project",
)
def create(
type: str | None,
name: str | None,
provider: str | None,
skip_provider: bool = False,
classic: bool = False,
declarative: bool = False,
) -> None:
"""Create a new crew, or flow."""
dmn_mode = is_dmn_mode_enabled()
@@ -194,6 +200,8 @@ def create(
if dmn_mode:
skip_provider = True
if type == "crew":
if declarative:
raise click.UsageError("--declarative can only be used with flow projects")
if classic:
from crewai_cli.create_crew import create_crew
@@ -205,7 +213,7 @@ def create(
elif type == "flow":
from crewai_cli.create_flow import create_flow
create_flow(name)
create_flow(name, declarative=declarative)
else:
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
@@ -512,10 +520,7 @@ def install(context: click.Context) -> None:
"--definition",
type=str,
default=None,
help=(
"Experimental: path to a Flow Definition YAML/JSON file, "
"or an inline YAML/JSON string."
),
help="Experimental: path to a declarative Flow YAML/JSON file.",
)
@click.option(
"--inputs",
@@ -537,7 +542,7 @@ def run(
"Warning: `crewai run --definition` is experimental and may change without notice.",
fg="yellow",
)
run_flow_definition(definition=definition, inputs=inputs)
run_declarative_flow(definition=definition, inputs=inputs)
return
run_crew(trained_agents_file=trained_agents_file)

View File

@@ -5,7 +5,10 @@ import click
from crewai_core.telemetry import Telemetry
def create_flow(name: str) -> None:
DECLARATIVE_FLOW_FOLDERS = ("crews", "tools", "knowledge", "skills")
def create_flow(name: str, *, declarative: bool = False) -> None:
"""Create a new flow."""
folder_name = name.replace(" ", "_").replace("-", "_").lower()
class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "")
@@ -20,6 +23,17 @@ def create_flow(name: str) -> None:
telemetry = Telemetry()
telemetry.flow_creation_span(class_name)
if declarative:
_create_declarative_flow(name, class_name, folder_name, project_root)
else:
_create_python_flow(name, class_name, folder_name, project_root)
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
def _create_python_flow(
name: str, class_name: str, folder_name: str, project_root: Path
) -> None:
(project_root / "src" / folder_name).mkdir(parents=True)
(project_root / "src" / folder_name / "crews").mkdir(parents=True)
(project_root / "src" / folder_name / "tools").mkdir(parents=True)
@@ -92,4 +106,41 @@ def create_flow(name: str) -> None:
fg="yellow",
)
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
def _create_declarative_flow(
name: str, class_name: str, folder_name: str, project_root: Path
) -> None:
project_root.mkdir(parents=True)
package_root = project_root / "src" / folder_name
package_root.mkdir(parents=True)
for folder in DECLARATIVE_FLOW_FOLDERS:
(package_root / folder).mkdir()
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")
for src_file in templates_dir.rglob("*"):
if not src_file.is_file():
continue
relative_path = src_file.relative_to(templates_dir)
dst_file = (
project_root / relative_path
if relative_path.name in {".gitignore", "README.md", "pyproject.toml"}
else package_root / relative_path
)
dst_file.parent.mkdir(parents=True, exist_ok=True)
content = src_file.read_text(encoding="utf-8")
content = content.replace("{{name}}", name)
content = content.replace("{{flow_name}}", class_name)
content = content.replace("{{folder_name}}", folder_name)
dst_file.write_text(content, encoding="utf-8")
(project_root / ".env").write_text("OPENAI_API_KEY=YOUR_API_KEY", encoding="utf-8")
(package_root / "__init__.py").write_text("", encoding="utf-8")
for folder in DECLARATIVE_FLOW_FOLDERS:
(package_root / folder / ".gitkeep").write_text("", encoding="utf-8")

View File

@@ -5,19 +5,27 @@ import click
def kickoff_flow() -> None:
"""
Kickoff the flow by running a command in the UV environment.
Kickoff the flow from declarative config or the Python UV entrypoint.
"""
command = ["uv", "run", "kickoff"]
from crewai_cli.run_declarative_flow import (
configured_project_declarative_flow,
run_declarative_flow_in_project_env,
)
try:
result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603
if definition := configured_project_declarative_flow():
run_declarative_flow_in_project_env(definition=definition)
else:
command = ["uv", "run", "kickoff"]
if result.stderr:
click.echo(result.stderr, err=True)
try:
subprocess.run( # noqa: S603
command, capture_output=False, text=True, check=True
)
except subprocess.CalledProcessError as e:
click.echo(f"An error occurred while running the flow: {e}", err=True)
click.echo(e.output, err=True)
except subprocess.CalledProcessError as e:
click.echo(f"An error occurred while running the flow: {e}", err=True)
raise SystemExit(1) from e
except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True)
except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True)
raise SystemExit(1) from e

View File

@@ -5,19 +5,27 @@ import click
def plot_flow() -> None:
"""
Plot the flow by running a command in the UV environment.
Plot the flow from declarative config or the Python UV entrypoint.
"""
command = ["uv", "run", "plot"]
from crewai_cli.run_declarative_flow import (
configured_project_declarative_flow,
plot_declarative_flow_in_project_env,
)
try:
result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603
if definition := configured_project_declarative_flow():
plot_declarative_flow_in_project_env(definition)
else:
command = ["uv", "run", "plot"]
if result.stderr:
click.echo(result.stderr, err=True)
try:
subprocess.run( # noqa: S603
command, capture_output=False, text=True, check=True
)
except subprocess.CalledProcessError as e:
click.echo(f"An error occurred while plotting the flow: {e}", err=True)
click.echo(e.output, err=True)
except subprocess.CalledProcessError as e:
click.echo(f"An error occurred while plotting the flow: {e}", err=True)
raise SystemExit(1) from e
except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True)
except Exception as e:
click.echo(f"An unexpected error occurred: {e}", err=True)
raise SystemExit(1) from e

View File

@@ -0,0 +1,212 @@
from __future__ import annotations
import json
from pathlib import Path
import subprocess
from typing import Any
import click
from crewai_cli.utils import build_env_with_all_tool_credentials
def run_declarative_flow_in_project_env(
definition: str, inputs: str | None = None
) -> None:
"""Run a declarative flow inside the project's Python environment."""
if is_declarative_flow_project_env() or not _has_project_file():
run_declarative_flow(definition=definition, inputs=inputs)
return
if inputs is not None:
raise click.UsageError("--inputs is only supported with --definition")
_execute_declarative_flow_command(["uv", "run", "crewai", "flow", "kickoff"])
def plot_declarative_flow_in_project_env(definition: str) -> None:
"""Plot a declarative flow inside the project's Python environment."""
if is_declarative_flow_project_env() or not _has_project_file():
plot_declarative_flow(definition=definition)
return
_execute_declarative_flow_command(["uv", "run", "crewai", "flow", "plot"])
def run_declarative_flow(definition: str, inputs: str | None = None) -> None:
"""Run a declarative flow from a YAML/JSON file path."""
parsed_inputs = _parse_inputs(inputs)
try:
flow = load_declarative_flow(definition)
result = flow.kickoff(inputs=parsed_inputs)
except Exception as exc:
click.echo(
f"An error occurred while running the declarative flow: {exc}", err=True
)
raise SystemExit(1) from exc
click.echo(_format_result(result))
def plot_declarative_flow(definition: str) -> None:
"""Plot a declarative flow from a YAML/JSON file path."""
try:
flow = load_declarative_flow(definition)
flow.plot()
except Exception as exc:
click.echo(
f"An error occurred while plotting the declarative flow: {exc}", err=True
)
raise SystemExit(1) from exc
def load_declarative_flow(definition: str) -> Any:
"""Load a declarative Flow instance from a YAML/JSON file path."""
try:
from crewai.flow.flow import Flow
from crewai.flow.flow_definition import FlowDefinition
except ImportError as exc:
click.echo(
"Running declarative flows requires the full crewai package.",
err=True,
)
raise SystemExit(1) from exc
definition_path = Path(definition).expanduser()
definition_source = _read_declarative_flow_source(definition_path, definition)
flow_definition = _parse_declarative_flow(
FlowDefinition,
definition_source,
source_path=definition_path,
)
return Flow.from_definition(flow_definition)
def configured_project_declarative_flow(
pyproject_data: dict[str, Any] | None = None,
) -> str | None:
"""Return the configured declarative flow source for flow projects."""
if pyproject_data is None:
try:
from crewai_cli.utils import read_toml
pyproject_data = read_toml()
except Exception:
return None
crewai_config = pyproject_data.get("tool", {}).get("crewai", {})
if crewai_config.get("type") != "flow":
return None
definition = crewai_config.get("definition")
if not isinstance(definition, str):
return None
return definition.strip() or None
def _execute_declarative_flow_command(command: list[str]) -> None:
env = build_env_with_all_tool_credentials()
try:
subprocess.run( # noqa: S603
command,
capture_output=False,
text=True,
check=True,
env=env,
)
except subprocess.CalledProcessError as e:
raise SystemExit(e.returncode) from e
except Exception as e:
click.echo(
f"An unexpected error occurred while running the declarative flow: {e}",
err=True,
)
raise SystemExit(1) from e
def is_declarative_flow_project_env() -> bool:
import os
return os.environ.get("UV_RUN_RECURSION_DEPTH") is not None
def _has_project_file(project_root: Path | None = None) -> bool:
root = project_root or Path.cwd()
return (root / "pyproject.toml").is_file()
def _parse_inputs(inputs: str | None) -> dict[str, Any] | None:
if inputs is None:
return None
try:
parsed = json.loads(inputs)
except json.JSONDecodeError as exc:
click.echo(f"Invalid --inputs JSON: {exc}", err=True)
raise SystemExit(1) from exc
if not isinstance(parsed, dict):
click.echo("Invalid --inputs JSON: expected an object.", err=True)
raise SystemExit(1)
return parsed
def _read_declarative_flow_source(path: Path, definition: str) -> str:
try:
if path.is_file():
source = _read_declarative_flow_file(path)
elif path.exists():
click.echo(
f"Invalid --definition path: {definition} is not a file.", err=True
)
raise SystemExit(1)
else:
click.echo(
f"Invalid --definition path: {definition} does not exist.", err=True
)
raise SystemExit(1)
except OSError as exc:
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
raise SystemExit(1) from exc
return source
def _read_declarative_flow_file(path: Path) -> str:
try:
source = path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
click.echo(
f"Unable to read --definition path {path}: {exc}",
err=True,
)
raise SystemExit(1) from exc
return source
def _parse_declarative_flow(
flow_definition_cls: type[Any], source: str, *, source_path: Path
) -> Any:
if _looks_like_json(source):
return flow_definition_cls.from_json(source, source_path=source_path)
return flow_definition_cls.from_yaml(source, source_path=source_path)
def _looks_like_json(source: str) -> bool:
stripped = source.lstrip()
return stripped.startswith("{")
def _format_result(result: Any) -> str:
raw_result = getattr(result, "raw", result)
if isinstance(raw_result, str):
return raw_result
try:
return json.dumps(raw_result, default=str)
except TypeError:
return str(raw_result)

View File

@@ -1,113 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import click
def run_flow_definition(definition: str, inputs: str | None = None) -> None:
"""Run a flow from a Flow Definition YAML/JSON string or file path."""
try:
from crewai.flow.flow import Flow
from crewai.flow.flow_definition import FlowDefinition
except ImportError as exc:
click.echo(
"Running flows from definitions requires the full crewai package.",
err=True,
)
raise SystemExit(1) from exc
parsed_inputs = _parse_inputs(inputs)
definition_source = _read_definition_source(definition)
try:
flow_definition = _parse_flow_definition(FlowDefinition, definition_source)
flow = Flow.from_definition(flow_definition)
result = flow.kickoff(inputs=parsed_inputs)
except Exception as exc:
click.echo(
f"An error occurred while running the flow definition: {exc}", err=True
)
raise SystemExit(1) from exc
click.echo(_format_result(result))
def _parse_inputs(inputs: str | None) -> dict[str, Any] | None:
if inputs is None:
return None
try:
parsed = json.loads(inputs)
except json.JSONDecodeError as exc:
click.echo(f"Invalid --inputs JSON: {exc}", err=True)
raise SystemExit(1) from exc
if not isinstance(parsed, dict):
click.echo("Invalid --inputs JSON: expected an object.", err=True)
raise SystemExit(1)
return parsed
def _read_definition_source(definition: str) -> str:
path = Path(definition).expanduser()
try:
is_file = path.is_file()
except OSError as exc:
if _looks_like_inline_definition(definition):
return definition
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
raise SystemExit(1) from exc
if is_file:
try:
return path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
click.echo(
f"Unable to read --definition path {path}: {exc}",
err=True,
)
raise SystemExit(1) from exc
try:
if path.exists():
click.echo(
f"Invalid --definition path: {definition} is not a file.", err=True
)
raise SystemExit(1)
except OSError as exc:
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
raise SystemExit(1) from exc
return definition
def _looks_like_inline_definition(definition: str) -> bool:
stripped = definition.lstrip()
return "\n" in definition or stripped.startswith(("{", "---")) or ":" in stripped
def _parse_flow_definition(flow_definition_cls: type[Any], source: str) -> Any:
if _looks_like_json(source):
return flow_definition_cls.from_json(source)
return flow_definition_cls.from_yaml(source)
def _looks_like_json(source: str) -> bool:
stripped = source.lstrip()
return stripped.startswith("{")
def _format_result(result: Any) -> str:
raw_result = getattr(result, "raw", result)
if isinstance(raw_result, str):
return raw_result
try:
return json.dumps(raw_result, default=str)
except TypeError:
return str(raw_result)

View File

@@ -0,0 +1,5 @@
.env
.venv/
__pycache__/
.crewai/
output/

View File

@@ -0,0 +1,17 @@
# {{name}} Flow
This project defines a CrewAI Flow in `src/{{folder_name}}/flow.yaml`.
## Install
```bash
crewai install
```
## Run
```bash
crewai flow kickoff
```
Edit `src/{{folder_name}}/flow.yaml` to change the flow. Add reusable crews under `src/{{folder_name}}/crews/`, custom Python tools under `src/{{folder_name}}/tools/`, and shared knowledge files under `src/{{folder_name}}/knowledge/`.

View File

@@ -0,0 +1,15 @@
schema: crewai.flow/v1
name: {{flow_name}}
description: A declarative CrewAI Flow.
state:
type: dict
default:
topic: AI agents
methods:
start:
start: true
do:
call: expression
expr: state.topic

View File

@@ -0,0 +1,20 @@
[project]
name = "{{folder_name}}"
version = "0.1.0"
description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.8a2"
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/{{folder_name}}"]
[tool.crewai]
type = "flow"
definition = "src/{{folder_name}}/flow.yaml"