n4nAI

CrewAI setup tutorial: API keys, models, and config

A hands-on tutorial for configuring CrewAI with API keys, model selection, and YAML configuration — from prerequisites to a working multi-agent crew.

n4n Team2 min read530 words

Audio narration

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

CrewAI turns LLM calls into structured multi-agent workflows, but the framework assumes you already have model access and credentials wired up. This tutorial walks through the complete setup: installing dependencies, managing API keys securely, selecting models via n4n.ai’s unified endpoint, and writing a minimal YAML config that drives a working crew. You’ll end with a runnable example that produces structured output you can inspect.

Prerequisites

  • Python 3.10 or newer
  • An OpenAI-compatible API key (we’ll use n4n.ai’s single endpoint for 240+ models)
  • Basic familiarity with YAML and Pydantic

Create a fresh virtual environment and install the core packages:

python -m venv .venv
source .venv/bin/activate
pip install crewai crewai-tools pyyaml python-dotenv

Managing API keys

Never hardcode secrets. Use a .env file at the project root and load it with python-dotenv before any CrewAI imports.

# .env
N4N_API_KEY=sk-your-key-here
N4N_BASE_URL=https://api.n4n.ai/v1
# config/loader.py
from pathlib import Path
from dotenv import load_dotenv

def load_env() -> None:
    env_path = Path(__file__).resolve().parents[1] / ".env"
    load_dotenv(dotenv_path=env_path, override=True)

Call load_env() as the very first line in your entrypoint script. This keeps credentials out of source control and works identically in local dev and containerized deployments.

Model configuration via YAML

CrewAI reads model parameters from a config/llms.yaml file when you pass llm_config to an Agent. Define one or more named profiles so you can swap models without touching code.

# config/llms.yaml
default:
  model: "openai/gpt-4o-mini"
  base_url: "https://api.n4n.ai/v1"
  api_key: "${N4N_API_KEY}"
  temperature: 0.2
  max_tokens: 2048

reasoning:
  model: "openai/o1-mini"
  base_url: "https://api.n4n.ai/v1"
  api_key: "${N4N_API_KEY}"
  temperature: 1.0
  max_tokens: 4096

The ${N4N_API_KEY} placeholder is resolved by CrewAI’s config loader when the environment variable is set. Using a single base URL means you can switch between GPT-4o, Claude, Llama, or any of the 240+ models without changing application code — only the model string.

Create a small utility to load the YAML and hand it to agents:

# config/models.py
import yaml
from pathlib import Path
from crewai import LLM

CONFIG_PATH = Path(__file__).with_name("llms.yaml")

def get_llm(profile: str = "default") -> LLM:
    with CONFIG_PATH.open() as f:
        cfg = yaml.safe_load(f)[profile]
    return LLM(**cfg)

Building a minimal crew

A crew needs agents, tasks, and a process. Start with two agents: a researcher that gathers facts and a writer that formats them. Each agent gets its own LLM profile.

# crew/agents.py
from crewai import Agent
from config.models import get_llm

researcher = Agent(
    role="Tech researcher",
    goal="Find accurate, up-to-date information on a given topic",
    backstory="You specialize in distilling technical documentation into concise summaries.",
    llm=get_llm("default"),
    verbose=True,
    allow_delegation=False,
)

writer = Agent(
    role="Technical writer",
    goal="Produce a clear, structured markdown report from research notes",
    backstory="You turn raw bullets into polished documentation engineers trust.",
    llm=get_llm("reasoning"),
    verbose=True,
    allow_delegation=False,
)

Define tasks with explicit output expectations. The output_json schema forces structured results you can parse downstream.

# crew/tasks.py
from crewai import Task
from crew.agents import researcher, writer
from pydantic import BaseModel, Field

class ResearchOutput(BaseModel):
    topic: str
    key_points: list[str] = Field(min_items=3, max_items=7)
    sources: list[str]

research_task = Task(
    description="Research {topic} and extract 5-7 key technical points with source URLs.",
    expected_output="A JSON object matching the ResearchOutput schema.",
    agent=researcher,
    output_json=ResearchOutput,
)

write_task = Task(
    description="Convert the research JSON into a markdown report with sections: Overview, Key Points, Sources.",
    expected_output="A complete markdown document ready to publish.",
    agent=writer,
    context=[research_task],
)

Wire everything together in a crew definition. The Process.sequential default passes each task’s output to the next.

# crew/crew.py
from crewai import Crew, Process
from crew.agents import researcher, writer
from crew.tasks import research_task, write_task

tech_crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, write_task],
    process=Process.sequential,
    verbose=True,
)

Entrypoint and execution

The main script loads environment variables, kicks off the crew, and prints the final markdown.

# main.py
from config.loader import load_env
load_env()  # must be first

from crew.crew import tech_crew

if __name__ == "__main__":
    result = tech_crew.kickoff(inputs={"topic": "CrewAI memory systems"})
    print(result.raw)

Run it:

python main.py

Expected output (truncated for brevity):

# CrewAI Memory Systems: Technical Overview

## Overview
CrewAI provides two memory layers: short-term (conversation history) and long-term (vector-backed persistence)...

## Key Points
- Short-term memory is scoped to a single crew execution and stored in-memory.
- Long-term memory uses ChromaDB by default; configurable via `memory_config`.
- Agents access memory through the `memory` attribute on `TaskOutput`.
- Memory can be shared across crews by passing the same `Memory` instance.
- Embedding model defaults to `text-embedding-3-small`; override in config.

## Sources
- https://docs.crewai.com/concepts/memory
- https://github.com/joaomdmoura/crewai/blob/main/src/crewai/memory

Verifying token usage

CrewAI exposes token counts on the CrewOutput object. Add a small wrapper to log them:

# main.py (add after kickoff)
usage = result.token_usage
print(f"Prompt tokens: {usage.prompt_tokens}, Completion tokens: {usage.completion_tokens}, Total: {usage.total_tokens}")

Typical output:

Prompt tokens: 1,842, Completion tokens: 1,105, Total: 2,947

These numbers come directly from the provider response headers. When routing through n4n.ai, the gateway forwards x-ratelimit-remaining and x-request-id headers so you can correlate usage with provider dashboards.

Common pitfalls

Missing environment variable — CrewAI raises ValueError: API key not found if .env isn’t loaded before the first LLM instantiation. Ensure load_env() runs at module import time in your entrypoint.

Model name mismatch — The model string must exactly match the provider’s identifier (e.g., openai/gpt-4o-mini, anthropic/claude-3-5-sonnet). Check the n4n.ai model catalog for the canonical list.

YAML indentation errors — Use two spaces, no tabs. Run yamllint config/llms.yaml in CI to catch drift.

Output schema validation failure — If the LLM returns malformed JSON, the task retries up to max_retry_limit (default 3). Increase temperature slightly or add few-shot examples to the task description.

Next steps

  • Add a third agent with allow_delegation=True to orchestrate sub-tasks.
  • Persist long-term memory by configuring memory_config in the crew constructor.
  • Wrap the crew in a FastAPI endpoint for production serving.
  • Set up Langfuse or LangSmith tracing by adding the callback handler to each agent’s llm config.

You now have a reproducible, config-driven CrewAI setup that separates credentials, model selection, and agent logic — ready to scale from prototype to production.

Tagscrewain4n-aiapi-keyssetup

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 getting started with n4n.ai posts →