Chapter 17 · CLI Engineering and YAML Configuration Projects
Chapter goals: Use
crewai createscaffolding to generate crew/flow projects, understand the config structure and variable interpolation rules ofconfig/agents.yaml+tasks.yaml, master the@CrewBase/@agent/@task/@crewdecorator system, and complete the development loop using common commands likecrewai run.
17.1 Why Move Role Definitions into YAML
Examples in chapters 1–16 define role/goal/backstory as Python strings. As projects grow, three problems surface:
- Prompt tuning requires反复 modifying code and redeploying;
- Non-programmers (product, ops) cannot participate in prompt iteration;
- Roles are coupled with code logic, making reuse and diff review difficult.
CrewAI's answer is configuration-code separation: put the roles' "personas" into YAML, keep orchestration logic in Python. The CLI scaffolding generates this structure in one command.
17.2 crewai create: Scaffold Quick Overview
# Create a classic Python/YAML-structured crew project
crewai create crew my_news_crew --classic
# Create a flow project (includes a sample crew)
crewai create flow my_pipeline
# Other scaffolds
crewai create tool my_tool # Custom tool repo skeleton
crewai create skill my-skill # Skill pack skeletonJSON-first is the new default
Starting from newer versions, crewai create crew generates a JSON-first project by default (crew.jsonc + agents/*.jsonc); to get the traditional crew.py + config/*.yaml structure used in this tutorial, add the --classic flag. Both forms are officially supported going forward.
Using my_news_crew --classic as an example, the generated directory structure looks like this:
my_news_crew/
├── pyproject.toml # Project dependencies and metadata (managed by uv)
├── README.md
├── .env # API Keys and other environment variables
└── src/my_news_crew/
├── __init__.py
├── main.py # Entry point: kickoff() / plot() / train() etc.
├── crew.py # Crew definition (decorator system)
└── config/
├── agents.yaml # Role personas
└── tasks.yaml # Task definitionsInstall dependencies and run:
cd my_news_crew
crewai install # Install dependencies declared in pyproject.toml using uv
crewai run # Run the current project (auto-detects crew or flow)17.3 agents.yaml and tasks.yaml Deep Dive
First clear the sample content, then write a real tech-news Crew. YAML keys correspond to Agent/Task constructor parameters:
# src/my_news_crew/config/agents.yaml
tech_researcher:
role: "Tech News Researcher"
goal: "Find the 3 most noteworthy tech news stories about {topic} this week"
backstory: "You have eight years of experience in tech media, sharp instincts, and trust only primary sources."
llm: "openai/deepseek-chat" # Optional: can also be injected uniformly in crew.py
max_iter: 15
verbose: true
news_writer:
role: "News Writer"
goal: "Rewrite research material into a newsletter suitable for WeChat Official Accounts"
backstory: "You are a seasoned new-media editor who excels at turning hardcore tech into accessible short articles."
verbose: true# src/my_news_crew/config/tasks.yaml
research_task:
description: "Research important tech news about topic '{topic}' this week, filter to 3 stories."
expected_output: >
3 news highlights, each including title, one-sentence summary, and source.
agent: tech_researcher
writing_task:
description: >
Write a Chinese tech newsletter based on the research results.
Colloquial tone, hook opening, body no more than 500 characters.
expected_output: "A Markdown-formatted newsletter for WeChat Official Accounts, including title."
agent: news_writerVariable interpolation is the soul of YAML config: curly-brace placeholders {topic} get replaced by the same-named key in kickoff(inputs={"topic": ...}). This lets one Crew serve different inputs in batch. Three things to note:
- Missing interpolation keys in inputs will raise an error directly; do not leave unsubstituted placeholders in YAML;
{xxx}does pure text replacement; expressions cannot be embedded;expected_outputuses YAML's>fold syntax to span multiple lines, ultimately joining into one string.
17.4 @CrewBase Decorator System
crew.py glues YAML config and Python orchestration together:
# src/my_news_crew/crew.py
import os
from crewai import Agent, Task, Crew, Process, LLM
from crewai.project import CrewBase, agent, task, crew
from crewai_tools import SerperDevTool # Example tool (requires pip install crewai-tools)
@CrewBase # Marker: auto-loads YAML from config/
class MyNewsCrew:
"""Tech newsletter Crew"""
agents_config = "config/agents.yaml" # Explicit config path declaration (default is this)
tasks_config = "config/tasks.yaml"
@agent # Each method corresponds to one key in YAML
def tech_researcher(self) -> Agent:
return Agent(
config=self.agents_config["tech_researcher"],
tools=[SerperDevTool()], # Tools still wired in code
llm=LLM(
model="openai/deepseek-chat",
base_url="https://api.deepseek.com/v1",
api_key=os.getenv("DEEPSEEK_API_KEY"),
),
)
@agent
def news_writer(self) -> Agent:
return Agent(
config=self.agents_config["news_writer"],
llm=LLM(
model="openai/deepseek-chat",
base_url="https://api.deepseek.com/v1",
api_key=os.getenv("DEEPSEEK_API_KEY"),
),
)
@task
def research_task(self) -> Task:
return Task(config=self.tasks_config["research_task"])
@task
def writing_task(self) -> Task:
return Task(config=self.tasks_config["writing_task"])
@crew # Assembly entry point
def crew(self) -> Crew:
return Crew(
agents=self.agents, # @CrewBase auto-collects all @agent methods
tasks=self.tasks, # Auto-collects all @task methods
process=Process.sequential,
verbose=True,
)Two ways to run — use the class directly, or go through the CLI:
# Approach 1: Execute in code (can pass interpolation variables)
result = MyNewsCrew().crew().kickoff(inputs={"topic": "AI Programming Assistants"})
print(result.raw)# Approach 2: crewai run finds main.py and executes per pyproject.toml declaration
crewai runOne-sentence summary of division of labor: YAML manages "who and what", Python manages "what tools, what model, how to assemble".
After config separation, there's an extra benefit: the same config can serve batch inputs. Paired with kickoff_for_each, run a batch of topics in one shot:
# batch_run.py — same Crew batches multiple interpolation inputs
from my_news_crew.crew import MyNewsCrew
# Each dict in the list serves as an independent kickoff input
batch_inputs = [
{"topic": "AI Programming Assistants"},
{"topic": "Vector Databases"},
{"topic": "Open-Source LLM Inference Optimization"},
]
results = MyNewsCrew().crew().kickoff_for_each(batch_inputs)
for inp, res in zip(batch_inputs, results):
print(f"== {inp['topic']} ==\n{res.raw}\n")kickoff_for_each executes each input sequentially (or concurrently per config) and returns a result list — a common entry point for content-factory scenarios; a single failure does not affect already-completed batch outputs, making it easy to resume from checkpoints.
17.5 main.py and Command Family
The scaffold-generated main.py usually looks like this (same for flow projects):
#!/usr/bin/env python
from my_news_crew.crew import MyNewsCrew
def run():
MyNewsCrew().crew().kickoff(inputs={"topic": "AI Programming Assistants"})
def train():
"""Training entry: called by crewai train"""
inputs = {"topic": "AI Programming Assistants"}
MyNewsCrew().crew().train(n_iterations=5, filename="trained_data.pkl", inputs=inputs)
if __name__ == "__main__":
run()Frequently used CLI commands overview:
| Command | Purpose |
|---|---|
crewai run | Run the current project (auto-detects crew or flow as of 0.103+) |
crewai install | Install dependencies per pyproject.toml |
crewai test -n 5 -m <model> | Run multiple iterations and score output quality with an LLM |
crewai train -n 10 | Human-feedback iterative training; produces human_feedback data |
crewai replay -t <task_id> | Replay execution from a specified task onward (debugging lifesaver) |
crewai log-tasks-outputs | View per-task outputs from the most recent kickoff |
crewai reset-memories -a | Clear memory/knowledge stores |
Version note
The new CLI consolidates old commands into resource groups (e.g., old crewai tool create x becomes crewai create tool x), and snake_case params (--n_iterations) become kebab-case (--n-iterations). Old syntax still works but prints deprecation warnings; use the new form for new projects.
17.6 Chapter Summary
- Config separation solves the engineering pain of prompt iteration: personas in YAML, tools and models in Python;
crewai create crew xxx --classicgenerates the classic YAML project; new default is JSON-first (crew.jsonc);- YAML keys map to constructor parameters;
{var}is interpolated bykickoff(inputs={...}); missing keys raise errors; @CrewBaseauto-loadsconfig/and collects@agent/@taskmethods;@crewreturns the assembled Crew;- High-frequency commands:
run/install/test/train/replay/log-tasks-outputs/reset-memories.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. If tasks.yaml has a {topic} placeholder, how do you provide its value at runtime?
2. Regarding the default behavior of crewai create crew, which is correct?
3. What are self.agents and self.tasks in an @CrewBase class?
4. To reproduce behavior after "a certain intermediate task" for debugging, which command do you use?
🛠️ Hands-On Exercises
- Scaffold a new "weekly report organization Crew" with
--classic; adapt the chapter example to readinputs={"week": "2025-W23"}; have at least two interpolations in YAML. - Intentionally misspell an agent's key name in agents.yaml (make it inconsistent with the config key returned by the
@agentmethod), observe the error message, then fix it — experience the typical config-error troubleshooting path. - Run the same project with both
crewai runand code-levelkickoff(); then usecrewai replayto replay from the second task once; compare the log differences among the three approaches.
A single Crew is now engineered. Next, organize multiple Crews into a complete business system: Chapter 18 · Multi-Crew Orchestration and Reuse.