Migration Guide: CrewAI → Bernstein¶
This guide walks you through migrating a CrewAI project to Bernstein. It covers concept mapping, code-to-config conversions, and the practical steps to get your first run working. Read it top-to-bottom the first time, then use the mapping tables as a reference.
Why teams migrate¶
CrewAI and Bernstein solve different problems. CrewAI orchestrates LLM agents in Python code. Bernstein orchestrates CLI coding agents (Claude Code, Codex, Gemini CLI, and others) with a deterministic Python control plane. If your goal is to automate multi-step software development tasks — write code, run tests, create PRs — Bernstein is a closer fit.
Key differences:
| Dimension | CrewAI | Bernstein |
|---|---|---|
| Orchestrator | LLM manager agent | Deterministic Python scheduler |
| Agent type | LLM API calls inside Python | CLI agents (Claude Code, Codex, etc.) |
| State | In-process (lost on crash) | Files in .sdd/ (survives restart) |
| Task parallelism | Controlled by manager LLM | Controlled by max_agents config |
| Cost of "thinking" | LLM tokens for every scheduling decision | Zero tokens for scheduling |
| Observability | Python logs | .sdd/ files, REST API, TUI dashboard |
| Agent lifecycle | Persistent sessions | Short-lived (spawn per task batch, then exit) |
Concept mapping¶
| CrewAI | Bernstein | Notes |
|---|---|---|
Agent(role=..., goal=..., backstory=...) | Role template + CLI adapter | Bernstein separates what a role does (YAML/Markdown template) from which agent executes it (adapter: claude, codex, gemini…) |
Task(description=..., expected_output=...) | Task file (.sdd/backlog/open/<id>.yaml) | Tasks are files, not Python objects. They survive crashes and can be retried. |
Crew(agents=[...], tasks=[...]) | Plan file (plans/<name>.yaml) | A plan describes stages and steps. The orchestrator executes it deterministically. |
Process.sequential | Stage depends_on chain | Sequential order is expressed as dependency edges, not a process flag. |
Process.hierarchical | Manager role + worker roles | A manager role decomposes high-level goals; workers execute the steps. |
@tool decorated function | Quality gate or MCP server | Per-task validation runs as quality gates. External capabilities plug in via MCP. |
crew.kickoff() | bernstein run | One command starts the orchestrator and spawns agents. |
Task.context=[task1] | depends_on: [task1-id] | Dependency declared in the task file, not in Python. |
| Agent memory | .sdd/ context files | Shared state is files on disk. Any agent can read them. |
CrewOutput | Completed task file + metrics | Results land in .sdd/backlog/done/ and .sdd/metrics/. |
Architecture: before and after¶
CrewAI: everything in Python¶
from crewai import Agent, Task, Crew, Process
researcher = Agent(
role='Senior Researcher',
goal='Find and synthesize information on AI trends',
backstory='Expert researcher with deep analytical skills',
verbose=True,
)
writer = Agent(
role='Content Writer',
goal='Write clear, compelling articles',
backstory='Experienced technical writer',
verbose=True,
)
research_task = Task(
description='Research the latest trends in multi-agent AI systems',
expected_output='A 3-paragraph summary with key findings',
agent=researcher,
)
write_task = Task(
description='Write an article based on the research findings',
expected_output='A 500-word article ready for publication',
agent=writer,
context=[research_task],
)
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
Problems at scale: The Python process must stay alive. If it crashes, all state is lost. The manager LLM burns tokens on every scheduling decision. Long sessions cause agent context drift.
Bernstein: config files + deterministic scheduler¶
Plan file (plans/ai-article.yaml):
id: ai-article
title: Research and write an AI trends article
stages:
- id: research
steps:
- id: research-trends
title: Research the latest trends in multi-agent AI systems
role: docs
goal: |
Produce a 3-paragraph summary of key trends in multi-agent AI systems.
Save findings to docs/research-notes.md.
complexity: medium
- id: writing
depends_on: [research]
steps:
- id: write-article
title: Write article from research notes
role: docs
goal: |
Using docs/research-notes.md, write a 500-word article.
Save to docs/ai-trends-article.md.
complexity: medium
Run it:
The orchestrator reads the plan, spawns a CLI agent for research-trends, waits for it to complete, then spawns a CLI agent for write-article. No LLM tokens spent on scheduling. If the process crashes mid-run, restart with:
Migration steps¶
Step 1: Map your agents to roles¶
List every CrewAI Agent you use. Map each to a Bernstein role. Bernstein ships with these built-in roles:
| CrewAI agent role | Bernstein role |
|---|---|
| Developer / Engineer | backend or frontend |
| QA / Tester | qa |
| Architect / Tech Lead | architect |
| Security Reviewer | security |
| DevOps / SRE | devops |
| Technical Writer / Docs | docs |
| Researcher / Analyst | analyst |
| Manager / Planner | manager |
If you need a custom role, add a file to templates/roles/<role-name>.md:
# Role: data-engineer
You are a data engineer. Your responsibilities:
- Design and implement data pipelines
- Write tests for all transforms
- Document schema changes
## Standards
- All pipelines must be idempotent
- Schema migrations require a rollback plan
Then reference it in tasks:
Step 2: Convert tasks to YAML files¶
Every CrewAI Task becomes a YAML file in .sdd/backlog/open/ or a step in a plan file.
CrewAI task:
Task(
description="Implement a rate-limiting middleware for the FastAPI app",
expected_output="Working middleware with tests, added to app/middleware/",
agent=backend_dev,
)
Bernstein task file (.sdd/backlog/open/rate-limiting.yaml):
id: rate-limiting
title: Implement rate-limiting middleware
role: backend
priority: p1
complexity: medium
goal: |
Implement rate-limiting middleware for the FastAPI app.
- Add to app/middleware/rate_limiter.py
- Write unit tests in tests/test_rate_limiter.py
- Register middleware in app/main.py
Expected: tests pass, no linting errors.
Or as a step in a plan:
- id: rate-limiting
title: Implement rate-limiting middleware
role: backend
priority: p1
complexity: medium
goal: |
Implement rate-limiting middleware for the FastAPI app.
Add to app/middleware/rate_limiter.py with tests.
Key difference: The goal field is what the CLI agent reads. Write it like a ticket — specific, actionable, with clear acceptance criteria.
Step 3: Replace Process.sequential with stage dependencies¶
CrewAI sequential process:
crew = Crew(
agents=[researcher, developer, qa],
tasks=[research_task, implement_task, test_task],
process=Process.sequential,
)
Bernstein plan with stage dependencies:
stages:
- id: research
steps:
- id: research-requirements
role: analyst
goal: "Document the feature requirements in docs/requirements.md"
- id: implementation
depends_on: [research]
steps:
- id: implement-feature
role: backend
goal: "Implement the feature described in docs/requirements.md"
- id: testing
depends_on: [implementation]
steps:
- id: write-tests
role: qa
goal: "Write integration tests for the feature. All tests must pass."
depends_on guarantees the stage doesn't start until all steps in the referenced stages are marked done.
Step 4: Replace Process.hierarchical with manager + worker pattern¶
CrewAI hierarchical process:
crew = Crew(
agents=[manager, dev1, dev2, qa],
tasks=[complex_task],
process=Process.hierarchical,
manager_llm=claude_opus,
)
Bernstein equivalent: Let the manager role decompose the goal. Add a high-level task:
- id: plan-feature
title: "Decompose the payment integration into tasks"
role: manager
goal: |
Create a detailed implementation plan for payment processing.
Break it into concrete tasks: schema migrations, API endpoints,
webhook handling, tests. Write the plan to docs/payment-plan.md.
complexity: high
Then create worker tasks that depend on it:
- id: payment-schema
title: "Create payment database schema"
role: backend
depends_on: [plan-feature]
goal: "Implement the database schema from docs/payment-plan.md"
- id: payment-api
title: "Implement payment API endpoints"
role: backend
depends_on: [payment-schema]
goal: "Build the API endpoints described in docs/payment-plan.md"
Or use bernstein run --goal "Integrate Stripe payments" and let Bernstein's planner decompose it automatically.
Step 5: Replace @tool with quality gates or MCP¶
CrewAI tool:
from crewai_tools import FileReadTool, SerperDevTool
@tool("Run test suite")
def run_tests(test_path: str) -> str:
"""Run pytest and return results."""
result = subprocess.run(["pytest", test_path], capture_output=True)
return result.stdout.decode()
agent = Agent(tools=[run_tests, FileReadTool(), SerperDevTool()])
Bernstein approach — quality gates for post-completion verification:
# bernstein.yaml
quality_gates:
lint: true
type_check: true
tests: true # runs pytest automatically after every task
security_scan: false
Bernstein approach — MCP servers for capabilities during execution:
# bernstein.yaml
mcp:
- name: filesystem
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
- name: brave-search
command: npx
args: ["-y", "@modelcontextprotocol/server-brave-search"]
env:
BRAVE_API_KEY: "${BRAVE_API_KEY}"
Quality gates verify output. MCP servers provide capabilities to the agent while it works. The distinction matters: quality gates run after the agent finishes, MCP servers are available during execution.
Step 6: Configure bernstein.yaml¶
The bernstein.yaml file is the equivalent of the Python script that instantiates your Crew. Put it in the root of your project:
# bernstein.yaml
cli: auto # auto-detect installed agents (claude, codex, gemini, etc.)
max_agents: 3 # run up to 3 agents in parallel
model_policy:
default: sonnet # default model for all roles
roles:
architect: opus # escalate to opus for high-stakes decisions
qa: haiku # haiku is sufficient for test writing
quality_gates:
lint: true
tests: true
type_check: true
budget:
max_usd: 10.00 # stop if total cost exceeds $10
Step 7: Run and verify¶
# From an existing CrewAI-style task file
bernstein run plans/my-workflow.yaml
# Or from a high-level goal (Bernstein plans it automatically)
bernstein run --goal "Refactor the authentication module to use JWTs"
# Monitor progress
bernstein status # snapshot
bernstein dashboard # live TUI
Pattern conversions¶
CrewAI: task with output file¶
Task(
description="Write a database schema for users",
expected_output="schema.sql file in db/migrations/",
output_file="db/migrations/001_users.sql",
agent=developer,
)
Bernstein:
id: users-schema
role: backend
goal: |
Write a PostgreSQL schema migration for the users table.
Create db/migrations/001_users.sql.
Include: id (uuid), email (unique), created_at, updated_at.
verification:
- type: file_exists
path: db/migrations/001_users.sql
CrewAI: multi-task with shared context¶
task2 = Task(
description="Write tests for the schema",
context=[task1], # receives task1's output
agent=qa,
)
Bernstein:
- id: test-schema
role: qa
depends_on: [users-schema] # task2 starts after task1 completes
goal: |
Write tests for db/migrations/001_users.sql.
Verify all constraints are tested.
Context flows through files. When users-schema completes, the QA agent reads the actual file. No in-process object passing needed.
CrewAI: parallel agents¶
# CrewAI doesn't parallelize by default — you work around it
crew = Crew(agents=[dev1, dev2], tasks=[task1, task2])
Bernstein: Tasks without depends_on run in parallel automatically, up to max_agents. No special configuration needed:
stages:
- id: parallel-work
steps:
- id: frontend-component # no depends_on → runs in parallel
role: frontend
goal: "Build the user profile card component"
- id: backend-endpoint # no depends_on → runs in parallel
role: backend
goal: "Implement the /api/users/:id endpoint"
- id: write-tests # no depends_on → runs in parallel
role: qa
goal: "Write unit tests for the profile service"
All three tasks above start at the same time (subject to max_agents).
CrewAI: conditional logic¶
def route_task(output):
if "error" in output.raw.lower():
return "fix_task"
return "review_task"
# CrewAI's router pattern
Bernstein: Quality gates handle the common case. If tests fail, the task is marked failed and requeued automatically. For custom routing:
# bernstein.yaml
quality_gates:
tests: true # blocks completion if tests fail
on_gate_failure: requeue # requeue for fix instead of abandoning
Troubleshooting¶
Tasks not being picked up¶
bernstein status
# Check that tasks show status: open
# Check that bernstein is running: bernstein status | grep orchestrator
If tasks are stuck in open state and no agent is claimed them, check: 1. bernstein.yaml exists in the project root 2. At least one CLI agent is installed (bernstein agents) 3. The agent is logged in (e.g., claude /login for Claude Code)
Wrong agent selected¶
Force a specific adapter in bernstein.yaml:
Or set per-task:
Task keeps failing quality gates¶
Check the gate output:
Then check the specific gate logs:
Costs are higher than expected¶
Reduce costs by routing lighter tasks to cheaper models:
model_policy:
roles:
qa: haiku # test writing doesn't need opus
docs: haiku # documentation tasks are low-stakes
What to read next¶
- Plans reference — full YAML schema for plan files
- Configuration reference — all bernstein.yaml options
- Role templates — built-in roles and how to extend them
- Quality gates — configuring verification
- Plugin SDK — extend Bernstein with custom hooks