Build an AI Internship Hunter
Orchestrate AI agents to find internships and generate application strategies.
Introduction
30 Second Summary
Searching for internships is exhausting. You spend hours scrolling through job boards, trying to figure out which roles match your skills, and wondering how to make each application stand out.
In this project, you will build a team of AI agents that automatically search for internships, evaluate your fit for each role, and generate a personalized application strategy.
What You'll Build
A multi-agent AI system powered by CrewAI that searches live internship listings, matches them to your skills, and produces a ranked application strategy report.
By the end of this project, you'll have:
- A CrewAI project with 3 specialized AI agents that collaborate in a sequential pipeline to research, evaluate, and strategize.
- A custom API tool that connects to the JSearch API and fetches real internship data from job boards.
- A personalized strategy report with ranked internships, qualification analysis, and tailored application advice for your top matches.
- Secret Mission: Add a Cover Letter Writer agent that drafts a personalized cover letter for your top-ranked internship.
Are there any prerequisites?
You should have basic Python knowledge (variables, functions, and loops). No prior experience with AI agents or APIs is needed.
Before We Start
Before We Start...
Install Python, uv, and CrewAI
To build your AI internship hunter, you need three tools working together: Python (the programming language), uv (a fast Python package manager), and CrewAI (the multi-agent framework).
You also need API keys that let your agents talk to real services. Your agents will use OpenAI's language model to think and the JSearch API to search for live internship postings.
In this step, get ready to:
- Install Python, uv, and the CrewAI CLI.
- Get an OpenAI API key and a RapidAPI key.
- Subscribe to the JSearch API free plan on RapidAPI.
Install Python and uv
Python is the language your agents run on. CrewAI requires Python 3.10 or higher (up to 3.13).
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check your Python version by running this command:
macOS
python3 --version
Windows
python --version
✔️ I see version 3.10 or higher
You're all set with Python. Move on to the uv installation below.
ⓧ I see an older version
You need Python 3.10 or higher for CrewAI to work. Download and install the latest version from the official Python website.
- Go to python.org/downloads and download the latest Python release.
- Run the installer. On Windows, check the Add python.exe to PATH box before clicking Install Now.
- Close and reopen your terminal, then re-run the version check command above.
ⓧ Command not found
Python is not installed on your system. Download and install it from the official website.
- Go to python.org/downloads and download the latest Python release.
- Run the installer. On Windows, check the Add python.exe to PATH box before clicking Install Now.
- Close and reopen your terminal, then run the version check command above to confirm installation.
Next, install uv. This is a fast Python package manager that CrewAI uses to manage dependencies and tools.
- Install uv by running this command:
macOS
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
What does this command do?
This downloads and runs the official uv installer from Astral (the company that makes uv). It places the uv binary on your system and adds it to your PATH so you can use it from any terminal window.
- Close and reopen your terminal so the PATH changes take effect.
- Verify uv is installed by running:
uv --version
You should see a version number printed in the terminal.
Don't see a version number?
Make sure you closed and reopened your terminal after installing uv. The PATH update only takes effect in new terminal sessions.
On Windows, if you still see an error, run uv tool update-shell and then restart your terminal.
Help me fix my uv installation
Install the CrewAI CLI
The CrewAI CLI is the command-line tool you use to create, manage, and run your AI agent crews.
- Install CrewAI by running this command:
uv tool install crewai
What does this command do?
The uv tool install command installs a Python CLI tool in an isolated environment. This means CrewAI gets its own clean space and won't conflict with other Python packages on your system.
- Verify the installation by running:
uv tool list
You should see crewai listed with a version number in the output.
CrewAI not showing up?
If uv tool list does not show crewai, try running uv tool update-shell and then restart your terminal. This ensures the tool's binary directory is on your PATH.
Help me get CrewAI installed
Get your API keys
Your agents need two API keys to function. The OpenAI key powers the AI thinking behind each agent. The RapidAPI key lets your tool search for real internship listings.
- Go to platform.openai.com and sign in (or create an account).
- Navigate to the API keys section in the left sidebar.
- Click Create new secret key.
- Copy the key and save it somewhere safe (you will not be able to see it again).
Does the OpenAI API cost money?
Yes, but this project uses gpt-4o-mini which costs $0.15 per million input tokens and $0.60 per million output tokens. A full run of your internship hunter crew costs well under $0.10.
New OpenAI accounts sometimes receive free credits. If your credits have expired, you will need to add a payment method in the Billing section of your OpenAI account.
Now get your RapidAPI key for the job search tool.
- Go to rapidapi.com and sign up for a free account (or sign in).
- Search for JSearch in the RapidAPI marketplace.
- Click on the JSearch API by OpenWeb Ninja.
- Click Subscribe to Test (or Pricing) and select the Basic plan ($0/month, 200 requests/month). No credit card is required.
- After subscribing, go to the Endpoints tab. Your RapidAPI key is shown in the code snippets under X-RapidAPI-Key. Copy this key.
What is the JSearch API?
JSearch aggregates job listings from sources like Google for Jobs, LinkedIn, Indeed, and more. The free Basic plan gives you 200 requests per month. Each time you run your crew, it uses about 1-2 API calls, so you have plenty of room for testing and iterating.
Your environment is ready. Next up, you'll scaffold your CrewAI project and explore its structure.
Scaffold Your CrewAI Project
Your tools are installed and your API keys are ready. Now it's time to create the actual project that your AI agents will live in.
CrewAI provides a scaffolding command that generates an entire project structure for you. This includes configuration files for your agents and tasks, a place to write custom tools, and an environment file for your secrets.
In this step, get ready to:
- Scaffold a new CrewAI crew project.
- Explore the generated file structure.
- Configure your API keys and model settings.
Create your CrewAI project
The CrewAI CLI has a create command that generates a full project skeleton with all the files you need.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your internship hunter project by running this command:
crewai create crew internship_hunter
What will this command ask me?
The CLI will show you a list of LLM providers to choose from, then ask you to select a model and enter an API key.
Select OpenAI as your provider, then choose gpt-4o-mini as your model. When prompted for an API key, paste your OpenAI key. You can update these values in the .env file later, so don't worry if you skip this step.
- Once the command finishes, navigate into your new project folder by running:
cd internship_hunter
- Confirm the project was created by running:
ls
You should see files like pyproject.toml, README.md, .env, and folders like src/ and knowledge/ in the output.
Command not found?
If you see an error saying crewai is not recognized, your shell may not have the updated PATH from Step 1. Close your terminal completely and reopen it, then try the command again.
help me fix a command not found error for crewai after installing it with uv tool install
Explore the project structure
CrewAI generated a complete project layout for you. Each file has a specific job in the multi-agent system.
- View the full project structure by running:
ls src/internship_hunter/
You should see main.py, crew.py, tools/, and config/ listed.
What does each file do?
- config/agents.yaml defines your AI agents with their roles, goals, and backstories.
- config/tasks.yaml defines the tasks your agents will perform and what output to expect.
- crew.py wires agents and tasks together into a crew that runs sequentially.
- main.py is the entry point that kicks off the crew with your input variables.
- tools/ is the directory where you'll add custom tools your agents can use.
- .env stores your API keys and model configuration securely.
Configure your API keys and model
The .env file is where CrewAI reads your API credentials and model settings from. You need to set your OpenAI key, your RapidAPI key, and tell CrewAI to use gpt-4o-mini as the default model.
- Open the internship_hunter project in your code editor.
- Open the .env file in the project root (the internship_hunter/ folder, not inside src/).
- Replace the entire contents of .env with the following:
OPENAI_API_KEY=your-openai-api-key-here
OPENAI_MODEL_NAME=gpt-4o-mini
RAPIDAPI_KEY=your-rapidapi-key-here
- Replace your-openai-api-key-here with the actual OpenAI API key you got in Step 1.
- Replace your-rapidapi-key-here with your RapidAPI key from Step 1.
- Save the file.
What does each line do?
- OPENAI_API_KEY authenticates your agents with OpenAI so they can use gpt-4o-mini for reasoning.
- OPENAI_MODEL_NAME tells CrewAI which model all agents should use by default.
- RAPIDAPI_KEY authenticates your custom tool with the JSearch API so it can fetch real internship data.
Not sure where to find your keys?
Your OpenAI key lives at platform.openai.com under **API keys**. Your RapidAPI key is on your RapidAPI dashboard under **Apps** in the default application's authorization section.
help me find my API keys for OpenAI and RapidAPI
✔️ Awesome, I've got everything!
Great. Double check you've saved your .env file before moving on.
ⓧ I'd like to double check the full code
Here is what your complete .env file should look like (with your own keys replacing the placeholders):
OPENAI_API_KEY=your-openai-api-key-here
OPENAI_MODEL_NAME=gpt-4o-mini
RAPIDAPI_KEY=your-rapidapi-key-here
Your project skeleton is ready and your credentials are configured. Next up, you'll build a custom tool that connects to the JSearch API and fetches real internship listings.
Build the JSearch API Tool
Your CrewAI project is scaffolded and your API keys are ready. Now it's time to give your agents a real superpower: the ability to search live internship listings from across the internet.
AI agents are only as useful as the tools they can access. Without a tool, your agents would be limited to general knowledge from their training data. In this step, you'll build a custom tool that connects to the JSearch API and returns real, current internship postings.
In this step, get ready to:
- Build a custom CrewAI tool that calls the JSearch API for live job data.
- Test the tool independently to confirm it returns real internship listings.
- Understand how the tool parses API responses into structured results.
Create the JSearch API tool file
In CrewAI, a tool is a Python class that your agents can call during their work. You define the tool's name, description, and a _run method that contains the actual logic.
- Add the requests package to your project by running this command:
uv add requests
What does this command do?
The uv add command adds requests to your project's pyproject.toml and installs it in the project's virtual environment. Your tool needs requests to make HTTP calls to the JSearch API.
- Create the tool file by running:
touch src/internship_hunter/tools/jsearch_tool.py
- Open src/internship_hunter/tools/jsearch_tool.py in your code editor.
- Copy the following code into the file:
import os
import requests
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type
class JSearchInput(BaseModel):
"""Input schema for JSearch tool."""
query: str = Field(..., description="Job search query including title and location, e.g. 'software engineering intern in New York'")
class JSearchTool(BaseTool):
name: str = "Search Internships"
description: str = "Searches for internship and job listings using the JSearch API. Provide a query with job title and location."
args_schema: Type[BaseModel] = JSearchInput
def _run(self, query: str) -> str:
url = "https://jsearch.p.rapidapi.com/search"
headers = {
"X-RapidAPI-Key": os.environ.get("RAPIDAPI_KEY"),
"X-RapidAPI-Host": "jsearch.p.rapidapi.com"
}
params = {
"query": query,
"num_pages": "1",
"date_posted": "month"
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
if data.get("status") != "OK" or not data.get("data"):
return "No internship listings found for the given query."
jobs = data["data"][:10]
results = []
for job in jobs:
result = (
f"Title: {job.get('job_title', 'N/A')}\n"
f"Company: {job.get('employer_name', 'N/A')}\n"
f"Location: {job.get('job_city', 'N/A')}, {job.get('job_state', 'N/A')}\n"
f"Type: {job.get('job_employment_type', 'N/A')}\n"
f"Description: {job.get('job_description', 'N/A')[:500]}\n"
f"Apply Link: {job.get('job_apply_link', 'N/A')}\n"
f"---"
)
results.append(result)
return "\n".join(results)
except requests.exceptions.RequestException as e:
return f"Error searching for internships: {str(e)}"
What does this code do?
This file defines a custom CrewAI tool with two classes:
- JSearchInput is a Pydantic model that validates the input your agents pass to the tool. It ensures the agent provides a search query string.
- JSearchTool inherits from BaseTool and implements the _run method. When an agent calls this tool, _run fires an HTTP request to the JSearch API with the agent's query.
- The name and description fields tell the AI agent what the tool is called and what it does. The agent reads these to decide when to use the tool.
- Save src/internship_hunter/tools/jsearch_tool.py.
Seeing a red underline on imports?
If your editor shows import errors for crewai or pydantic, the project's virtual environment might not be synced with your editor yet. Run crewai install in your terminal to sync dependencies, then reload your editor window.
help me fix import errors in my CrewAI project
✔️ Awesome, I've got everything!
Great. Double check you've saved jsearch_tool.py before moving on.
ⓧ I'd like to double check the full code
import os
import requests
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type
class JSearchInput(BaseModel):
"""Input schema for JSearch tool."""
query: str = Field(..., description="Job search query including title and location, e.g. 'software engineering intern in New York'")
class JSearchTool(BaseTool):
name: str = "Search Internships"
description: str = "Searches for internship and job listings using the JSearch API. Provide a query with job title and location."
args_schema: Type[BaseModel] = JSearchInput
def _run(self, query: str) -> str:
url = "https://jsearch.p.rapidapi.com/search"
headers = {
"X-RapidAPI-Key": os.environ.get("RAPIDAPI_KEY"),
"X-RapidAPI-Host": "jsearch.p.rapidapi.com"
}
params = {
"query": query,
"num_pages": "1",
"date_posted": "month"
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
if data.get("status") != "OK" or not data.get("data"):
return "No internship listings found for the given query."
jobs = data["data"][:10]
results = []
for job in jobs:
result = (
f"Title: {job.get('job_title', 'N/A')}\n"
f"Company: {job.get('employer_name', 'N/A')}\n"
f"Location: {job.get('job_city', 'N/A')}, {job.get('job_state', 'N/A')}\n"
f"Type: {job.get('job_employment_type', 'N/A')}\n"
f"Description: {job.get('job_description', 'N/A')[:500]}\n"
f"Apply Link: {job.get('job_apply_link', 'N/A')}\n"
f"---"
)
results.append(result)
return "\n".join(results)
except requests.exceptions.RequestException as e:
return f"Error searching for internships: {str(e)}"
Test the tool independently
Before wiring this tool into your agents, it's worth running a quick standalone test to confirm the API connection works and your key is valid.
- Test your tool by running this command:
uv run --env-file .env python -c "from internship_hunter.tools.jsearch_tool import JSearchTool; tool = JSearchTool(); print(tool._run('software engineering intern in United States'))"
What does this command do?
The uv run command runs Python inside your project's virtual environment so all installed packages are available. The --env-file .env flag loads your API keys from the .env file into the environment before executing.
The inline Python script imports your tool class, creates an instance, and calls its _run method directly with a sample query.
- Check your terminal output. You should see a list of internship results with titles, companies, locations, and apply links printed to the console.
Seeing an error instead of results?
- If you see a 403 or 401 error, double check that your RAPIDAPI_KEY in .env is correct and that you subscribed to the JSearch API's free Basic plan on RapidAPI.
- If you see a module not found error, make sure you ran uv add requests earlier in this step.
- If you see "No internship listings found," try a broader query like intern in United States to confirm your API connection works.
help me debug my JSearch tool test
Review the response parsing
Now that you've confirmed the tool works, take a closer look at how it processes the raw API response into a format your agents can reason about.
How does the tool parse API responses?
The JSearch API returns a JSON object with a data array containing up to hundreds of job listings. Your tool takes only the first 10 results with data["data"][:10] to keep things focused.
For each job, the tool extracts seven fields: job_title, employer_name, job_city, job_state, job_employment_type, job_description (truncated to 500 characters), and job_apply_link. These are the fields your Qualification Matcher and Strategy Advisor agents will analyze in later steps.
Truncating descriptions to 500 characters keeps the agent's context window efficient. Full job descriptions can be thousands of characters, which would waste tokens without adding much value for matching.
Why the try/except block?
The try/except wrapping requests.exceptions.RequestException catches network failures, timeouts, and HTTP errors gracefully. Instead of crashing the entire crew run, the tool returns a readable error message that the agent can report or retry.
Your custom tool is built and battle-tested. Next up, you'll define the AI agents that will use this tool to research, match, and strategize about internships for you.
Define Your AI Agents
Your JSearch tool is ready to fetch live internship data from the web. Now it's time to build the AI team that puts that data to work.
In CrewAI, each agent is defined with a role, goal, and backstory in a YAML configuration file. These three fields tell the agent who it is, what it should accomplish, and how it should approach problems.
In this step, get ready to:
- Configure a Job Researcher agent that searches for internships.
- Configure a Qualification Matcher agent that evaluates your fit.
- Configure a Strategy Advisor agent that creates application plans.
Configure the Job Researcher agent
The Job Researcher is the first agent in your crew. Its job is to search for real internship listings using the JSearch tool you built in the previous step.
- Open src/internship_hunter/config/agents.yaml in your editor.
- Select all the placeholder content and delete it.
- Paste the following configuration for your first agent:
job_researcher:
role: >
Internship Research Specialist
goal: >
Find the most relevant {field} internships in {location} that match
the candidate's background and interests.
backstory: >
You are an expert career researcher who specializes in finding hidden
internship opportunities. You know how to search effectively and filter
results to find the best matches. You always use your search tool to
find real, current listings.
What does this configuration do?
Each agent in CrewAI needs three fields that define its personality and purpose:
- role is the agent's job title. It tells the LLM what kind of expert it's pretending to be.
- goal defines what the agent is trying to achieve. Notice the {field} and {location} placeholders. CrewAI replaces these with real values at runtime.
- backstory gives the agent context about how to behave. A detailed backstory produces better, more focused outputs.
Why role-based agents instead of one big prompt?
A single mega-prompt that tries to search, evaluate, and strategize all at once produces unfocused results. Splitting responsibilities into specialized agents means each one can focus deeply on its task.
The Job Researcher only cares about finding listings. It doesn't need to evaluate fit or write strategies. This focused scope leads to higher quality output from each agent.
You won't see a change yet. The next chunk adds the Qualification Matcher agent below this one.
Add the Qualification Matcher agent
The Qualification Matcher takes the internship listings found by the researcher and evaluates how well your skills align with each role.
- In agents.yaml, place your cursor at the end of the job_researcher backstory (after the last line of text).
- Add a blank line, then paste this second agent configuration:
qualification_matcher:
role: >
Qualification Analysis Expert
goal: >
Evaluate how well the candidate's skills ({skills}) match each internship
and identify gaps that need to be addressed.
backstory: >
You are a career counselor with deep knowledge of what employers look for
in intern candidates. You can quickly assess a candidate's fit for a role
and provide honest, actionable feedback about their strengths and gaps.
What does this agent do?
The Qualification Matcher receives the researcher's output and compares each listing's requirements against {skills}. This placeholder will be filled with your actual skills when you run the crew later.
This agent doesn't need the JSearch tool. It only analyzes data that's already been collected by the researcher.
You won't see a change yet. The next chunk adds the final agent.
Add the Strategy Advisor agent
The Strategy Advisor is the last agent in your crew. It takes the matched results and creates an actionable application plan for your top internships.
- In agents.yaml, place your cursor at the end of the qualification_matcher backstory.
- Add a blank line, then paste this third agent configuration:
strategy_advisor:
role: >
Application Strategy Consultant
goal: >
Create a prioritized application plan with specific strategies for each
internship to maximize the candidate's chances of getting hired.
backstory: >
You are a seasoned career strategist who has helped hundreds of candidates
land competitive internships. You know what makes applications stand out
and how to position candidates effectively for each specific role.
What does this agent do?
The Strategy Advisor takes the Qualification Matcher's analysis and produces your final deliverable. It creates a prioritized plan with specific talking points and tips for each application.
Notice how each agent builds on the previous one's output. This is the sequential workflow pattern: researcher finds data, matcher evaluates it, advisor turns it into strategy.
- Save agents.yaml.
- Confirm your file has three top-level keys: job_researcher, qualification_matcher, and strategy_advisor. Each should have role, goal, and backstory fields indented beneath it.
Seeing indentation errors?
YAML is sensitive to spacing. Make sure each role, goal, and backstory key is indented with exactly 2 spaces under its agent name. The text below each > marker should be indented with 4 spaces.
Also check that there are no tabs mixed with spaces. YAML does not allow tab characters for indentation.
help me fix YAML indentation errors in my agents.yaml file
✔️ Awesome, I've got everything!
Great. Double check you've saved your file.
ⓧ I'd like to double check the full code
Here's the complete agents.yaml file with all three agents:
job_researcher:
role: >
Internship Research Specialist
goal: >
Find the most relevant {field} internships in {location} that match
the candidate's background and interests.
backstory: >
You are an expert career researcher who specializes in finding hidden
internship opportunities. You know how to search effectively and filter
results to find the best matches. You always use your search tool to
find real, current listings.
qualification_matcher:
role: >
Qualification Analysis Expert
goal: >
Evaluate how well the candidate's skills ({skills}) match each internship
and identify gaps that need to be addressed.
backstory: >
You are a career counselor with deep knowledge of what employers look for
in intern candidates. You can quickly assess a candidate's fit for a role
and provide honest, actionable feedback about their strengths and gaps.
strategy_advisor:
role: >
Application Strategy Consultant
goal: >
Create a prioritized application plan with specific strategies for each
internship to maximize the candidate's chances of getting hired.
backstory: >
You are a seasoned career strategist who has helped hundreds of candidates
land competitive internships. You know what makes applications stand out
and how to position candidates effectively for each specific role.
Your crew now has three specialized agents ready to go. Next up, you'll define the tasks that tell each agent exactly what to do and wire everything together.
Define Tasks and Wire the Crew
Your agents know who they are, but they don't have instructions yet. In CrewAI, tasks define what each agent should accomplish and in what order the work flows.
In this step, you'll define three tasks that chain together: search for internships, match your qualifications, then create a strategy report. You'll also wire everything together in crew.py and set up main.py with your personal search criteria.
In this step, get ready to:
- Define tasks in YAML that tell each agent what to accomplish.
- Wire crew.py to connect agents, tools, and tasks in a sequential pipeline.
- Set up main.py with your search criteria and run the crew.
Define your tasks in tasks.yaml
Each task tells one agent exactly what to do, what format the output should take, and how much detail to include. Tasks are the instructions that turn your agents from idle specialists into active workers.
- Open src/internship_hunter/config/tasks.yaml in your editor.
- Replace all existing content with the following:
search_internships:
description: >
Search for {field} internships in {location} using the JSearch tool.
Find at least 5 current internship listings. For each listing, extract:
- Job title
- Company name
- Location
- Key requirements
- Application link
Focus on entry-level and internship positions suitable for students.
expected_output: >
A structured list of at least 5 internship listings with title, company,
location, key requirements, and application links.
agent: job_researcher
match_qualifications:
description: >
Analyze each internship listing from the research results against the
candidate's skills: {skills}.
For each listing, provide:
- Match score (1-10)
- Matching skills
- Skill gaps
- Overall fit assessment
expected_output: >
A ranked list of internships with match scores, matching skills,
skill gaps, and fit assessments for each position.
agent: qualification_matcher
create_strategy:
description: >
Based on the qualification analysis, create a prioritized application
strategy. For the top 3 internships:
- Why this role is a good fit
- Key talking points for the application
- How to address any skill gaps
- Specific tips for standing out
- Recommended timeline for applying
expected_output: >
A comprehensive application strategy report in markdown format with
prioritized recommendations and specific action items for each top internship.
agent: strategy_advisor
output_file: output/strategy_report.md
What does this code do?
Each task has three core fields:
- description tells the agent exactly what to do. The {field}, {location}, and {skills} placeholders get filled in at runtime from your inputs.
- expected_output defines the format and content the agent should produce.
- agent assigns the task to a specific agent you defined in agents.yaml.
The create_strategy task also has output_file which saves the final report to a markdown file automatically.
- Save tasks.yaml.
YAML not saving correctly?
- YAML is indentation-sensitive. Make sure each task name starts at the beginning of the line (no leading spaces) and child fields are indented with exactly two spaces.
- The > character after each field name means the text wraps into a single paragraph. Make sure there's a space between the colon and the >.
Help me fix a YAML indentation error in my tasks.yaml file.
Wire up crew.py to connect agents and tasks
The crew file is the orchestrator. It imports your tool, creates each agent with the right configuration, and chains tasks together in a sequential pipeline.
- Open src/internship_hunter/crew.py in your editor.
- Replace all existing content with the following:
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from crewai.agents.agent_builder.base_agent import BaseAgent
from typing import List
from internship_hunter.tools.jsearch_tool import JSearchTool
@CrewBase
class InternshipHunterCrew:
"""Internship Hunter crew that finds and analyzes internships."""
agents: List[BaseAgent]
tasks: List[Task]
agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml"
@agent
def job_researcher(self) -> Agent:
return Agent(
config=self.agents_config["job_researcher"],
tools=[JSearchTool()],
verbose=True,
)
@agent
def qualification_matcher(self) -> Agent:
return Agent(
config=self.agents_config["qualification_matcher"],
verbose=True,
)
@agent
def strategy_advisor(self) -> Agent:
return Agent(
config=self.agents_config["strategy_advisor"],
verbose=True,
)
@task
def search_internships(self) -> Task:
return Task(
config=self.tasks_config["search_internships"],
)
@task
def match_qualifications(self) -> Task:
return Task(
config=self.tasks_config["match_qualifications"],
)
@task
def create_strategy(self) -> Task:
return Task(
config=self.tasks_config["create_strategy"],
output_file="output/strategy_report.md",
)
@crew
def crew(self) -> Crew:
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=True,
)
What does this code do?
- @CrewBase marks this class as a CrewAI crew. It automatically loads your YAML config files and wires agents to tasks by matching method names to YAML keys.
- The job_researcher agent gets JSearchTool() in its tools list. This is the only agent that needs a tool because it performs the actual API search.
- Process.sequential means tasks run one after another in the order they're defined. Each task's output becomes context for the next.
- output_file="output/strategy_report.md" on the final task saves the strategy advisor's output to a file you can read after the crew finishes.
- Save crew.py.
Why do method names matter here?
CrewAI uses a naming convention to connect everything. The method name job_researcher must match the key job_researcher in agents.yaml. The same applies for tasks.
If these names don't match, CrewAI can't find the configuration and your crew will fail to start.
Seeing an import error?
- Make sure the import path from internship_hunter.tools.jsearch_tool import JSearchTool matches your file location exactly.
- Check that jsearch_tool.py is inside src/internship_hunter/tools/ and not in the project root.
Help me debug a CrewAI import error in crew.py.
Update main.py with your search inputs
The main file is where you define what you're searching for. The input variables you set here flow into the {field}, {location}, and {skills} placeholders in your agent goals and task descriptions.
- Open src/internship_hunter/main.py in your editor.
- Replace all existing content with the following:
#!/usr/bin/env python
import sys
from internship_hunter.crew import InternshipHunterCrew
def run():
"""Run the internship hunter crew."""
inputs = {
"field": "software engineering",
"location": "United States",
"skills": "Python, JavaScript, React, Git, SQL, REST APIs, problem-solving, teamwork",
}
print("\n\U0001f50d Starting Internship Hunter Crew...\n")
print(f" Field: {inputs['field']}")
print(f" Location: {inputs['location']}")
print(f" Skills: {inputs['skills']}")
print("\n" + "=" * 50 + "\n")
result = InternshipHunterCrew().crew().kickoff(inputs=inputs)
print("\n" + "=" * 50)
print("\n\u2705 Crew finished! Check output/strategy_report.md for your strategy.\n")
return result
if __name__ == "__main__":
run()
What does this code do?
- The inputs dictionary holds your search criteria. You can customize field, location, and skills to match your own background.
- InternshipHunterCrew().crew().kickoff(inputs=inputs) creates the crew, builds all agents and tasks, then starts the sequential pipeline with your inputs.
- The print statements give you progress feedback in the terminal while the crew runs.
- Save main.py.
- Install your project dependencies by running this command in your terminal:
crewai install
- Run the crew by entering this command:
crewai run
You should see output in your terminal showing each agent starting its task, thinking through the problem, and passing results to the next agent.
Crew failing to start?
- Make sure your .env file has valid values for OPENAI_API_KEY and RAPIDAPI_KEY. Placeholder text won't work.
- Check that your method names in crew.py match the keys in your YAML files exactly (e.g. job_researcher in both places).
- If you see a module not found error, confirm you ran crewai install before crewai run.
Help me debug why my CrewAI crew is failing to start.
Customize your search
The default inputs search for software engineering internships. Feel free to change the field, location, and skills values in main.py to match your own interests and background.
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files before moving on.
ⓧ I'd like to double check the full code
Here's the full content of each file for comparison.
search_internships:
description: >
Search for {field} internships in {location} using the JSearch tool.
Find at least 5 current internship listings. For each listing, extract:
- Job title
- Company name
- Location
- Key requirements
- Application link
Focus on entry-level and internship positions suitable for students.
expected_output: >
A structured list of at least 5 internship listings with title, company,
location, key requirements, and application links.
agent: job_researcher
match_qualifications:
description: >
Analyze each internship listing from the research results against the
candidate's skills: {skills}.
For each listing, provide:
- Match score (1-10)
- Matching skills
- Skill gaps
- Overall fit assessment
expected_output: >
A ranked list of internships with match scores, matching skills,
skill gaps, and fit assessments for each position.
agent: qualification_matcher
create_strategy:
description: >
Based on the qualification analysis, create a prioritized application
strategy. For the top 3 internships:
- Why this role is a good fit
- Key talking points for the application
- How to address any skill gaps
- Specific tips for standing out
- Recommended timeline for applying
expected_output: >
A comprehensive application strategy report in markdown format with
prioritized recommendations and specific action items for each top internship.
agent: strategy_advisor
output_file: output/strategy_report.md
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from crewai.agents.agent_builder.base_agent import BaseAgent
from typing import List
from internship_hunter.tools.jsearch_tool import JSearchTool
@CrewBase
class InternshipHunterCrew:
"""Internship Hunter crew that finds and analyzes internships."""
agents: List[BaseAgent]
tasks: List[Task]
agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml"
@agent
def job_researcher(self) -> Agent:
return Agent(
config=self.agents_config["job_researcher"],
tools=[JSearchTool()],
verbose=True,
)
@agent
def qualification_matcher(self) -> Agent:
return Agent(
config=self.agents_config["qualification_matcher"],
verbose=True,
)
@agent
def strategy_advisor(self) -> Agent:
return Agent(
config=self.agents_config["strategy_advisor"],
verbose=True,
)
@task
def search_internships(self) -> Task:
return Task(
config=self.tasks_config["search_internships"],
)
@task
def match_qualifications(self) -> Task:
return Task(
config=self.tasks_config["match_qualifications"],
)
@task
def create_strategy(self) -> Task:
return Task(
config=self.tasks_config["create_strategy"],
output_file="output/strategy_report.md",
)
@crew
def crew(self) -> Crew:
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=True,
)
#!/usr/bin/env python
import sys
from internship_hunter.crew import InternshipHunterCrew
def run():
"""Run the internship hunter crew."""
inputs = {
"field": "software engineering",
"location": "United States",
"skills": "Python, JavaScript, React, Git, SQL, REST APIs, problem-solving, teamwork",
}
print("\n\U0001f50d Starting Internship Hunter Crew...\n")
print(f" Field: {inputs['field']}")
print(f" Location: {inputs['location']}")
print(f" Skills: {inputs['skills']}")
print("\n" + "=" * 50 + "\n")
result = InternshipHunterCrew().crew().kickoff(inputs=inputs)
print("\n" + "=" * 50)
print("\n\u2705 Crew finished! Check output/strategy_report.md for your strategy.\n")
return result
if __name__ == "__main__":
run()
Your crew is alive and working. In the next step, you'll review the full output and learn how to iterate on your agents' prompts to improve the results.
Run Your Crew and Review Results
Your agents are configured, your tasks are defined, and your crew is wired together. Now it is time to see them work as a team.
In this step, you will launch the full crew and watch as each agent takes its turn. The Job Researcher searches for live internships, the Qualification Matcher scores your fit, and the Strategy Advisor writes a personalized application plan.
In this step, get ready to:
- Run the crew and observe the sequential agent collaboration.
- Review the generated strategy report and console output.
- Iterate on agent prompts to improve output quality.
Run the full crew
The crewai run command reads your main.py entry point, initializes the crew, and kicks off all three tasks in sequence. Each agent processes its task before handing the result to the next agent in line.
- Launch your crew by running this command:
crewai run
What happens when you run this?
The sequential process means tasks execute one after another. First, the job_researcher calls the JSearch API to find internships. Then, the qualification_matcher scores each listing against your skills. Finally, the strategy_advisor writes your application plan and saves it to output/strategy_report.md.
Because verbose=True is set on every agent and on the crew, you will see detailed logs of each agent's thinking and tool usage in real time.
How long does this take?
The crew typically takes 1-3 minutes to complete all three tasks. This depends on the OpenAI API response time and the JSearch API call. Watch the terminal as each agent works through its task.
- Wait for the crew to finish. You should see each agent's name and role printed as it starts working, followed by its reasoning and final output.
- When the crew completes, you should see a message that says Crew finished! Check output/strategy_report.md for your strategy. in the terminal.
Seeing an API error?
If you see an error mentioning your OpenAI API key, double-check the OPENAI_API_KEY value in your .env file. Make sure it is a valid key with billing enabled on your OpenAI account.
If you see an error related to the JSearch API, verify your RAPIDAPI_KEY in .env and confirm you are subscribed to the JSearch API Basic plan on RapidAPI.
help me debug an API error when running my CrewAI crew
Review the strategy report and console output
The real output of your crew lives in two places: the console log (showing how agents collaborated) and the final markdown file (your deliverable).
- Open output/strategy_report.md in your code editor to read the final report.
- You should see a markdown document with ranked internships, match scores, and a personalized application strategy for the top 3 positions.
What should I see in the report?
Your strategy_report.md should contain sections for each top internship with details like why the role is a good fit, key talking points, how to address skill gaps, and tips for standing out. The exact internships will vary based on what the JSearch API returns at the time you run it.
- Scroll back through the terminal output to examine the agent interactions.
- Look for where the job_researcher called the JSearch tool and returned internship listings.
- Look for where the qualification_matcher received those listings and scored each one.
- Look for where the strategy_advisor used the match analysis to write the final strategy.
Why does the console show so much text?
The verbose=True setting enables detailed execution logs for debugging. You can see each agent's thought process, tool calls, and final answers. This is extremely useful for understanding how agents collaborate and for finding issues in your prompts.
Iterate on agent prompts to improve results
The quality of your crew's output depends directly on how well you wrote each agent's goal and backstory, and each task's description. Small tweaks to the wording can produce dramatically different results.
- Open src/internship_hunter/config/agents.yaml in your editor.
- Try updating the skills value in src/internship_hunter/main.py to match your actual skills and interests.
- Try changing the field and location values in main.py to target a different area or region.
- Run the crew again to see the updated results:
crewai run
Effective prompt iteration
If the strategy report feels too generic, try adding more specifics to the strategy_advisor backstory in agents.yaml. For example, you could add that it specializes in tech companies, or that it focuses on first-time applicants.
If the internship results are not relevant, refine the search_internships task description in tasks.yaml to add more specific filters or keywords.
Results not matching your expectations?
Make sure you saved all YAML files before running crewai run again. YAML is sensitive to indentation, so ensure your edits maintain the same spacing.
help me improve my CrewAI agent prompts
You have a fully working multi-agent system that searches for real internships and creates personalized application strategies. Ready to push it further? The Secret Mission adds a Cover Letter Writer agent that drafts a custom cover letter for your top match.
Secret mission
Add a Cover Letter Writer Agent
Your crew finds internships and builds a strategy, but wouldn't it be great if it also wrote the first draft of your cover letter? In this mission, you'll add a fourth agent that takes your top-ranked internship and generates a personalized, ready-to-send cover letter.
Clean Up Your Resources
Clean Up Your Resources
This project runs locally and uses two external APIs. The JSearch API is on a free plan with no charges, but your OpenAI API key is connected to a paid account that charges per request.
Cost warning
Your OpenAI API key remains active after this project. Each time you run the crew, it costs approximately $0.05-0.10. If you do not plan to use the key again, delete it from your OpenAI dashboard to prevent any unexpected charges.
Resources you used:
- The internship_hunter project folder on your Desktop.
- An OpenAI API key stored in your .env file.
- A RapidAPI account with the JSearch API Basic plan subscription.
- CrewAI CLI installed via uv tool install crewai.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Keep everything running
No action needed. Choose this if you are still actively building or plan to run the crew again with different search criteria.
- Your project folder, API keys, and subscriptions stay as they are.
- Your OpenAI API key will only incur charges when you actively run the crew.
- Your JSearch Basic plan stays free as long as you remain under 200 requests per month.
- The CrewAI CLI remains available on your system for future projects.
Pause - I'll come back to this later
Free up resources while keeping your files intact so you can pick up where you left off.
- No running processes to stop. The crew only runs when you explicitly call crewai run.
- Consider removing your API keys from the .env file and storing them in a password manager until you return.
- Your project folder and code remain on your Desktop ready to resume.
Delete - I don't want to use this again
Remove all project resources and revoke access to external services.
- Delete your project folder by running this command:
cd ~/Desktop
rm -rf internship_hunter
On Windows, run this instead:
cd ~\Desktop
Remove-Item -Recurse -Force internship_hunter
- Delete your OpenAI API key. Go to https://platform.openai.com/api-keys and click the trashcan icon next to the key you created for this project.
- Unsubscribe from the JSearch API on RapidAPI. Go to your Developer Dashboard, select Billing, then Subscriptions & Usage, and click Unsubscribe next to the JSearch API.
- Uninstall the CrewAI CLI by running:
uv tool uninstall crewai
What about Python and uv?
Python and uv are general-purpose tools you will likely use in future projects. There is no need to remove them unless you are sure you will not use them again.
Nice Work!
Nice Work!
You just built an AI-powered internship hunting system using CrewAI. Your crew of specialized agents can search live job listings, evaluate your qualifications, and produce a personalized application strategy.
You've learned how to:
- Built a multi-agent system with three specialized agents (Job Researcher, Qualification Matcher, and Strategy Advisor) that collaborate in a sequential workflow.
- Created a custom CrewAI tool that connects to the JSearch API to fetch real internship data from sources like LinkedIn, Indeed, and Glassdoor.
- Configured role-based agents with YAML definitions and wired them together with task delegation to produce a ranked strategy report.
- Secret Mission: Added a Cover Letter Writer agent that drafts personalized cover letters for your top-ranked internship match.
Ready to quiz yourself?