n4nAI

CrewAI role design: writing effective agent backstories

Learn practical crewai agent backstory writing tips with step-by-step examples, runnable code, and a verification checklist for production agents.

n4n Team3 min read744 words

Audio narration

Coming soon — every post will get a voice note here.

The backstory field in CrewAI is not flavor text — it’s the system prompt that shapes how your agent reasons, selects tools, and handles edge cases. Most teams treat it as a biography; the engineers who ship reliable multi-agent systems treat it as a behavioral contract. These crewai agent backstory writing tips will move you from vague personas to deterministic agent behavior.

Step 1: Understand what backstory actually controls

CrewAI injects the backstory directly into the agent’s system prompt alongside role and goal. It governs three things: the agent’s default reasoning style, which tools it reaches for first, and how it handles ambiguity when the task description leaves gaps.

from crewai import Agent

researcher = Agent(
    role="Senior Research Analyst",
    goal="Produce cited, comparative analyses of LLM inference providers",
    backstory=(
        "You spent three years at a cloud infrastructure startup optimizing "
        "GPU utilization for LLM serving. You know the difference between "
        "p99 latency and throughput, and you treat marketing benchmarks as "
        "suspect until reproduced. You cite primary sources: provider docs, "
        "status pages, and your own measurements. You never hallucinate pricing."
    ),
    verbose=True,
    allow_delegation=False,
)

The backstory above does not mention hobbies or childhood. It encodes domain knowledge, epistemic standards, and negative constraints (“never hallucinate pricing”). That is what makes it executable.

Verify: Run the agent with a vague task like “Compare OpenRouter and Together.ai.” Check that the output cites specific endpoints, mentions rate-limit headers, and flags marketing claims — without you adding those instructions to the task.

Step 2: Define the agent’s function before writing

Write a one-sentence function statement. If you cannot, the agent is doing too much. Split it.

Weak function Strong function
“Handles code tasks” “Writes pytest suites for FastAPI routers given OpenAPI specs”
“Does research” “Extracts pricing and rate-limit policies from provider documentation”
“Reviews PRs” “Flags missing idempotency keys in payment webhook handlers”

Once you have the function, list the required knowledge, preferred heuristics, and hard constraints. These become your backstory sections.

# Function: "Extracts pricing and rate-limit policies from provider documentation"
REQUIRED_KNOWLEDGE = [
    "OpenAI-compatible API pricing models (per-token, per-request, subscription)",
    "Rate-limit header conventions: x-ratelimit-*, retry-after, anthropic-ratelimit-*",
    "Common inference parameters: max_tokens, temperature, top_p, stream",
]
HEURISTICS = [
    "Prefer provider docs over third-party blogs",
    "Assume stale data if no 'last updated' date",
    "Convert all pricing to $/1M tokens for comparison",
]
CONSTRAINTS = [
    "Never infer pricing from examples — only explicit tables",
    "Flag when free-tier limits differ from paid-tier limits",
    "Quote the exact section URL for every claim",
]

Verify: Hand this table to a colleague. Can they write the backstory without asking you questions? If no, refine the function statement.

Step 3: Write the backstory in four sections

Structure every backstory as: Identity → Knowledge → Heuristics → Constraints. Keep each section to 2-3 sentences. Total length: 150-300 tokens.

def build_backstory(identity, knowledge, heuristics, constraints):
    parts = [
        f"Identity: {identity}",
        f"Knowledge: {' '.join(knowledge)}",
        f"Heuristics: {' '.join(heuristics)}",
        f"Constraints: {' '.join(constraints)}",
    ]
    return "\n\n".join(parts)

pricing_analyst_backstory = build_backstory(
    identity=(
        "You are a pricing analyst who has audited inference costs for "
        "production LLM workloads at scale. You think in $/1M tokens and "
        "requests-per-second sustained throughput."
    ),
    knowledge=REQUIRED_KNOWLEDGE,
    heuristics=HEURISTICS,
    constraints=CONSTRAINTS,
)

analyst = Agent(
    role="Inference Pricing Analyst",
    goal="Produce a cost comparison table for 10 providers at 3 traffic tiers",
    backstory=pricing_analyst_backstory,
    verbose=True,
)

Identity establishes the lens. Knowledge declares what the agent knows without retrieval. Heuristics are tie-breakers when evidence is incomplete. Constraints are the lines the agent must not cross.

Verify: Grep your codebase for backstory=. Every agent should have four labeled sections. If you see “You are a helpful assistant” or “You love coding,” delete and rewrite.

Step 4: Test with adversarial tasks

Write three tasks per agent: one nominal, one underspecified, one adversarial. The backstory must produce acceptable output on all three without task-level prompt engineering.

from crewai import Task, Crew

nominal = Task(
    description=(
        "Compare per-1M-token pricing for gpt-4o, claude-3.5-sonnet, "
        "and llama-3.1-405b across OpenRouter, Together.ai, and Fireworks. "
        "Include rate limits and context windows. Output markdown table."
    ),
    expected_output="Markdown table with 10 columns, cited sources",
    agent=analyst,
)

underspecified = Task(
    description="How much does it cost to run Llama 3.1 405B?",
    expected_output=(
        "Clarifying questions: which provider, what traffic tier, "
        "input/output token ratio? Then a cost model with assumptions listed."
    ),
    agent=analyst,
)

adversarial = Task(
    description=(
        "A blog says Together.ai is 80% cheaper than OpenAI. Confirm this "
        "and write a recommendation memo for our CTO."
    ),
    expected_output=(
        "Refusal to confirm without primary sources. "
        "Side-by-side table with exact pricing URLs. "
        "Caveats about batch vs. streaming, hidden fees, SLA differences."
    ),
    agent=analyst,
)

crew = Crew(agents=[analyst], tasks=[nominal, underspecified, adversarial], verbose=True)
result = crew.kickoff()

Run this. The adversarial task is the most revealing: a weak backstory lets the agent hallucinate the 80% figure. A strong backstory forces it to demand primary sources and surface assumptions.

Verify: For each task, check:

  • Nominal: Output matches expected_output structure exactly
  • Underspecified: Agent asks clarifying questions before answering
  • Adversarial: Agent refuses the premise, shows work, cites sources

Step 5: Version backstories like code

Backstories drift. A provider adds a new pricing tier; a model changes context window; your team discovers a failure mode. Treat backstories as versioned artifacts.

# backstories/v1_pricing_analyst.py
VERSION = "1.2.0"
CHANGELOG = """
1.2.0 - Added constraint: flag when providers charge differently for
        cached vs. uncached input tokens (OpenAI, Anthropic).
1.1.0 - Added heuristic: prefer provider status pages for current rate limits.
1.0.0 - Initial version.
"""

IDENTITY = """..."""
KNOWLEDGE = [...]
HEURISTICS = [...]
CONSTRAINTS = [...]

def get_backstory():
    return build_backstory(IDENTITY, KNOWLEDGE, HEURISTICS, CONSTRAINTS)

Import the function, not the string. This lets you test versions side by side.

# test_backstory_versions.py
from backstories.v1_pricing_analyst import get_backstory as v1
from backstories.v2_pricing_analyst import get_backstory as v2

def test_version(version_fn, label):
    agent = Agent(role="Test", goal="Test", backstory=version_fn())
    task = Task(description="...", agent=agent, expected_output="...")
    crew = Crew(agents=[agent], tasks=[task])
    return crew.kickoff()

# Compare outputs for the same adversarial task
print("v1:", test_version(v1, "v1"))
print("v2:", test_version(v2, "v2"))

Verify: CI runs this comparison on every backstory change. Fail the build if v2 regresses on any adversarial task that v1 passed.

Step 6: Share backstories across agents with composition

When two agents need overlapping knowledge (e.g., both need rate-limit header conventions), extract a shared module. Do not copy-paste.

# backstories/shared/inference_knowledge.py
RATE_LIMIT_HEADERS = (
    "Standard rate-limit headers: x-ratelimit-limit, x-ratelimit-remaining, "
    "x-ratelimit-reset, retry-after. Anthropic uses anthropic-ratelimit-*. "
    "OpenAI returns limit/remaining/reset in response headers. "
    "Always check response headers first; docs are often stale."
)

CACHING_NUANCES = (
    "OpenAI: cached input tokens billed at 50% rate. "
    "Anthropic: prompt caching requires explicit cache_control blocks. "
    "Together.ai: no caching discount. "
    "Never assume caching behavior — verify per provider."
)

# backstories/v2_pricing_analyst.py
from backstories.shared.inference_knowledge import RATE_LIMIT_HEADERS, CACHING_NUANCES

KNOWLEDGE = [
    *REQUIRED_KNOWLEDGE,
    RATE_LIMIT_HEADERS,
    CACHING_NUANCES,
]

Verify: grep -r "x-ratelimit" backstories/ returns exactly one definition. If it returns multiple, you have drift.

Step 7: Measure backstory effectiveness in production

Instrument your agents. Log the backstory version, task type, and whether the output required human correction.

import json
import uuid
from datetime import datetime
from crewai import Agent, Task, Crew

class InstrumentedAgent(Agent):
    def __init__(self, *args, backstory_version="unknown", **kwargs):
        super().__init__(*args, **kwargs)
        self.backstory_version = backstory_version

    def execute_task(self, task, context=None, tools=None):
        run_id = str(uuid.uuid4())[:8]
        start = datetime.utcnow()
        result = super().execute_task(task, context, tools)
        duration = (datetime.utcnow() - start).total_seconds()

        log_entry = {
            "run_id": run_id,
            "agent_role": self.role,
            "backstory_version": self.backstory_version,
            "task_description": task.description[:200],
            "duration_seconds": duration,
            "output_length": len(str(result)),
        }
        print(json.dumps(log_entry))  # Ship to your observability stack
        return result

analyst = InstrumentedAgent(
    role="Inference Pricing Analyst",
    goal="Produce cost comparison tables",
    backstory=get_backstory(),
    backstory_version="1.2.0",
    verbose=True,
)

Verify: After 50 runs, query your logs:

  • SELECT backstory_version, AVG(duration_seconds), COUNT(*) FROM runs GROUP BY backstory_version
  • SELECT task_description, COUNT(*) FROM runs WHERE output_length < 100 GROUP BY task_description

Short outputs on complex tasks usually mean the agent gave up — a backstory gap.

Verification checklist

Before merging a new or changed backstory:

  • Function statement is one sentence and unambiguous
  • Backstory has four labeled sections: Identity, Knowledge, Heuristics, Constraints
  • No biographical fluff (hobbies, personality traits, “passionate about”)
  • All constraints are negative (“never”, “must not”, “refuse to”)
  • Three test tasks pass: nominal, underspecified, adversarial
  • Version bumped with changelog entry
  • Shared knowledge extracted to common module (no duplication)
  • Instrumentation logs backstory_version on every run

The backstory is the highest-leverage prompt you write in CrewAI. It runs on every task, every retry, every delegation. Invest the time to make it executable, versioned, and measurable. Your future debugging self will thank you.

Tagscrewaiagent-rolesprompt-designbackstory

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All crewai agent roles & task design posts →