Add experimental crewai run --definition for flows (#6147)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled

Let users run a Flow from a Flow Definition YAML file or inline string
without writing Python, passing kickoff inputs as `--inputs` JSON. The
flag is gated behind an experimental warning since the definition format
may still change.
This commit is contained in:
Vini Brasil
2026-06-12 22:31:05 -07:00
committed by GitHub
parent 6ad821b157
commit d80719df81
4 changed files with 338 additions and 2 deletions

View File

@@ -13,6 +13,7 @@ from crewai_cli.cli import (
flow_add_crew,
login,
reset_memories,
run,
test,
train,
version,
@@ -119,6 +120,43 @@ def test_test_invalid_string_iterations(evaluate_crew, runner):
)
@mock.patch("crewai_cli.cli.run_crew")
def test_run_uses_project_runner_by_default(run_crew, runner):
result = runner.invoke(run)
assert result.exit_code == 0
run_crew.assert_called_once_with(trained_agents_file=None)
assert "experimental" not in result.output.lower()
@mock.patch("crewai_cli.cli.run_flow_definition")
def test_run_with_definition_uses_definition_runner(run_flow_definition, runner):
result = runner.invoke(
run,
["--definition", "flow.yaml", "--inputs", '{"topic":"AI"}'],
)
assert result.exit_code == 0
assert (
"Warning: `crewai run --definition` is experimental and may change without notice."
in result.output
)
run_flow_definition.assert_called_once_with(
definition="flow.yaml", inputs='{"topic":"AI"}'
)
@mock.patch("crewai_cli.cli.run_crew")
@mock.patch("crewai_cli.cli.run_flow_definition")
def test_run_rejects_inputs_without_definition(run_flow_definition, run_crew, runner):
result = runner.invoke(run, ["--inputs", '{"topic":"AI"}'])
assert result.exit_code == 2
assert "Error: --inputs requires --definition" in result.output
run_flow_definition.assert_not_called()
run_crew.assert_not_called()
@mock.patch("crewai_cli.cli.AuthenticationCommand")
def test_login(command, runner):
mock_auth = command.return_value

View File

@@ -0,0 +1,156 @@
from __future__ import annotations
import json
import sys
import types
import pytest
import yaml
from crewai_cli.run_flow_definition import run_flow_definition
class _FakeFlow:
def __init__(self, definition):
self.definition = definition
def kickoff(self, inputs=None):
return {
"flow": self.definition["name"],
"inputs": inputs or {},
}
class _FakeFlowFactory:
@classmethod
def from_definition(cls, definition):
return _FakeFlow(definition)
class _FakeFlowDefinition:
@classmethod
def from_yaml(cls, source):
return yaml.safe_load(source)
@classmethod
def from_json(cls, source):
return json.loads(source)
@pytest.fixture
def fake_flow_runtime(monkeypatch):
crewai_module = types.ModuleType("crewai")
flow_package = types.ModuleType("crewai.flow")
flow_module = types.ModuleType("crewai.flow.flow")
flow_definition_module = types.ModuleType("crewai.flow.flow_definition")
flow_module.Flow = _FakeFlowFactory
flow_definition_module.FlowDefinition = _FakeFlowDefinition
monkeypatch.setitem(sys.modules, "crewai", crewai_module)
monkeypatch.setitem(sys.modules, "crewai.flow", flow_package)
monkeypatch.setitem(sys.modules, "crewai.flow.flow", flow_module)
monkeypatch.setitem(
sys.modules, "crewai.flow.flow_definition", flow_definition_module
)
def _captured_json(capsys):
return json.loads(capsys.readouterr().out)
def test_run_flow_definition_reads_definition_file(
tmp_path, capsys, fake_flow_runtime
):
definition_path = tmp_path / "flow.yaml"
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
run_flow_definition(str(definition_path), '{"topic":"AI"}')
assert _captured_json(capsys) == {
"flow": "TestFlow",
"inputs": {"topic": "AI"},
}
@pytest.mark.parametrize(
("definition_source", "expected_flow_name"),
[
pytest.param(
"schema: crewai.flow/v1\nname: InlineFlow\n",
"InlineFlow",
id="inline-yaml",
),
pytest.param(
'{"schema":"crewai.flow/v1","name":"InlineJsonFlow"}',
"InlineJsonFlow",
id="inline-json",
),
pytest.param(
'{"schema":"crewai.flow/v1","name":"' + ("JsonFlow" * 500) + '"}',
"JsonFlow" * 500,
id="large-inline-json",
),
],
)
def test_run_flow_definition_accepts_inline_definitions(
definition_source, expected_flow_name, capsys, fake_flow_runtime
):
run_flow_definition(definition_source)
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
@pytest.mark.parametrize(
("filename", "definition_source", "expected_flow_name"),
[
pytest.param(
"flow.yaml",
"schema: crewai.flow/v1\nname: YamlFileFlow\n",
"YamlFileFlow",
id="yaml-file",
),
pytest.param(
"flow.json",
'{"schema":"crewai.flow/v1","name":"JsonFlow"}',
"JsonFlow",
id="json-file",
),
],
)
def test_run_flow_definition_accepts_definition_files(
filename, definition_source, expected_flow_name, tmp_path, capsys, fake_flow_runtime
):
definition_path = tmp_path / filename
definition_path.write_text(definition_source)
run_flow_definition(str(definition_path))
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
def test_run_flow_definition_rejects_non_object_inputs(fake_flow_runtime, capsys):
with pytest.raises(SystemExit):
run_flow_definition("name: TestFlow", '["not", "an", "object"]')
assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err
def test_run_flow_definition_reports_unreadable_file(
monkeypatch, tmp_path, capsys, fake_flow_runtime
):
definition_path = tmp_path / "flow.yaml"
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
def raise_permission_error(self, *args, **kwargs):
raise PermissionError("no access")
monkeypatch.setattr("pathlib.Path.read_text", raise_permission_error)
with pytest.raises(SystemExit):
run_flow_definition(str(definition_path))
err = capsys.readouterr().err
assert "Unable to read --definition path" in err
assert str(definition_path) in err
assert "no access" in err