My Dashboard Web Apps AI Agents My Projects My Profile
← Dashboard
πŸ€– Track 2 of 2 Β· AI Agents

AI
Agents

From a no-code automation to a fully autonomous agent running 24/7 in the cloud β€” six projects, zero experience required.

Choose a project to start
1
Starter
Your First Agent β€” No-Code Β· 1–2 hrs
Build a working AI automation using zero code β€” just Claude and Zapier. Your agent responds to triggers, answers questions, and takes actions automatically.
Claude Free Zapier 100% Free No Code
Start Project 1 β†’
2
Beginner
Tool-Use Agent Β· 1–3 hrs
Learn the most important pattern in agent development β€” the tool-use loop. Build a Python agent with real tools: web search, file writing, and date lookup.
Python Anthropic SDK Tool Use Agent Loop
Start Project 2 β†’
3
Intermediate
Extended Thinking Agent Β· 2–4 hrs
Use Claude's extended thinking feature to build a deep analysis agent. Claude reasons step-by-step through complex problems before answering β€” visible in the thinking blocks you'll read.
Python claude-opus-4-6 Extended Thinking Deep Analysis
Start Project 3 β†’
4
Advanced
Multi-Agent Orchestration Β· 3–6 hrs
Build three specialized agents β€” Researcher, Writer, Reviewer β€” that pass structured JSON to each other. The pipeline retries automatically when the reviewer scores too low.
Python claude-sonnet-4-6 Structured Outputs Orchestration
Start Project 4 β†’
5
Pro
MCP: Claude Connected to Everything Β· 4–7 hrs
Configure built-in MCP servers to give Claude access to your filesystem, Supabase database, and GitHub. Then build your own custom MCP server in Python.
Claude Code MCP Python Supabase
Start Project 5 β†’
6
Ninja
Production Agent + Computer Use Β· 5–9 hrs
Use Claude's computer use API to build an agent that browses the web autonomously. Then deploy a production agent to Railway with Docker, cron scheduling, and live logs.
Python Computer Use API Railway Docker
Start Project 6 β†’
Project 1 of 6
1
Starter
Your First Agent β€” No-Code
Build a working AI automation using zero code β€” just Claude and Zapier. Your agent will respond to triggers, answer questions, and take actions automatically. No terminal, no installation, no experience needed.
Claude Free Zapier 100% Free to Start No Code No Installation
πŸ’‘ What you can build with no-code agents
Email auto-responder
Form β†’ AI β†’ Slack reply
Daily briefing emailer
Lead qualifier bot
Meeting notes summarizer
Social post generator
Support ticket tagger
Document classifier
Newsletter curator
Customer feedback analyzer
Invoice data extractor
RSS β†’ summary digest
πŸ”“ Skills you'll unlock
What agents are
Triggers vs actions
Zapier workflows
Prompt engineering
Connecting apps with AI
Testing agent behavior
API cost awareness
Agent safety basics
πŸ’° Understanding API Costs β€” Before You Build Anything β–Ύ

This is the most important thing to understand before building agents. Unlike web apps that run on free Vercel hosting, agents call APIs every time they run β€” and those calls cost money. Understanding costs upfront prevents nasty surprises.

1
How Claude API pricing works
You pay per token β€” roughly per word. Claude Sonnet 4 costs approximately $0.003 per 1,000 input tokens and $0.015 per 1,000 output tokens. A typical agent call costs about $0.01–$0.05. Check current pricing at anthropic.com/pricing.
2
Estimate your agent's monthly cost before deploying
The formula: (runs per day) Γ— (cost per run) Γ— 30. An agent that runs 24 times per day (hourly) at $0.02 per run = $14.40/month. Always calculate this before setting a schedule.
3
Set a spending limit before you start
In the Anthropic Console, go to Billing β†’ Usage Limits and set a monthly hard limit β€” e.g. $20. This caps your exposure if an agent runs in an unexpected loop.
4
Keep prompts short and efficient
Every word in your system prompt costs money, multiplied by every run. Ask Claude: "Rewrite this system prompt to be as concise as possible while keeping all the important instructions."
✍️ Prompt Engineering for Agents β€” Different from Chatting β–Ύ

Writing prompts for an autonomous agent is very different from chatting with Claude. In a chat, you can clarify and correct. In an agent, the prompt runs unattended β€” every ambiguity becomes a bug.

1
Give the agent a clear role and scope
Start every agent system prompt with: what the agent is, what it should do, and β€” crucially β€” what it should NOT do.
You are an email response assistant for [Business Name]. Your job: read incoming customer support emails and draft a helpful reply. ONLY respond to questions about our products and services. DO NOT make promises about refunds, timelines, or pricing without checking. If you're unsure how to respond, say "I'll need to check on this" instead of guessing.
2
Specify the output format exactly
Agents pass their output to the next step β€” a webhook, an email, a database. If the format varies, downstream steps break. Tell the agent exactly what to return: "Respond with only a JSON object with keys: reply_text (string), confidence (1-10), needs_human_review (boolean). No other text."
3
Test edge cases before going live
Before turning on an automated agent, manually test at least 5 edge cases: empty input, very long input, input in another language, rude or nonsensical input. Ask Claude: "What are 5 edge cases that might cause this agent to behave unexpectedly?"
🎯 What You're Building β€” and What an Agent Actually Is β–Ύ

An agent is an AI that doesn't just answer questions β€” it takes action. Instead of you typing a prompt and reading a response, an agent watches for a trigger, processes it using AI, and does something with the result β€” all without you touching a keyboard.

What you'll build: A Zapier workflow that watches your Gmail inbox for emails with the subject "AI Help", feeds the email body to Claude, and sends an intelligent AI-written reply automatically β€” within seconds of receiving the email.
βš™οΈ Step 1 β€” Set Up Your Accounts β–Ύ
1
Get Claude API access
Go to console.anthropic.com and sign up. Add a small credit (e.g. $5) to get started.
2
Create a free Zapier account
Go to zapier.com and sign up. The free plan allows 100 tasks per month β€” more than enough to build and test your first agent.
3
Get your Anthropic API key
In the Anthropic Console, go to API Keys β†’ Create Key. Copy it somewhere safe β€” you'll paste it into Zapier. Never share this key publicly.
πŸ€– Step 2 β€” Build Your First Zap (Agent Workflow) β–Ύ
1
Create a new Zap in Zapier
Log in to zapier.com. Click the orange + Create button in the top left and select New Zap. You'll land in the Zap editor. At the top of the page, click the name field and type AI Email Responder so you can find it easily later.
2
Set up the Gmail trigger
Before you build anything, send yourself a test email first. Open Gmail and send an email to yourself with the subject AI Help and a question in the body like "How do I reset my password?". You'll need this later when testing.
πŸ“‹ Now set up the trigger in Zapier:
1. In the Zap editor, click the Trigger box. Search for Gmail and select it.
2. Under Event, select New Email and click Continue.
3. Click Sign in to Gmail, connect your account, and click Continue.
4. On the Configure tab you will see a Label or Mailbox dropdown. Click it and select INBOX from the list. Click Continue.
5. Click Test trigger. Zapier will load recent emails from your inbox. Find and select the AI Help test email you just sent yourself, then click Continue with selected record. This ensures all the following steps use the right test data.
3
Add a Filter β€” only run for "AI Help" emails
The trigger will fire for every email in your inbox. You need a Filter so the Zap only continues when the subject contains AI Help.
Click the + icon below the Gmail trigger step. Search for Filter and select it. The action will be called "Only continue if…"
You will see three fields. Set them up exactly like this:
Field 1 (dropdown): Click and select 1. Subject β€” the subject line from your Gmail trigger.
Field 2 (dropdown): Click and select (Text) Contains.
Field 3 (text input): Type AI Help.
Click Continue then Test. Because you selected the AI Help email in Step 2, Zapier should say "Zap would have continued". If it says it would NOT have continued, go back to Step 2 and make sure you selected the correct test email.
4
Add Claude β€” generate the reply
Click the + icon below the Filter step. Search for Anthropic (Claude) and select it. For the event choose Send Message and click Continue. Connect your Anthropic account using your API key and click Continue.
πŸ“‹ Setting up the User Message field:

Click inside the User Message field and type this exactly:
You are a helpful assistant. Someone emailed asking for help. Read their email and write a friendly, helpful reply in 2-3 sentences. Email subject: {{1. Subject}} Email body: {{1. Body Plain}} Write only the reply text β€” no subject line, no greeting like "Dear", just the reply body.
To insert the blue tokens, use the + button on the right side of the field to open the variable picker. Under Previous Steps click 1. New Email and select Subject for the first token, and Body Plain (NOT Body Html) for the second token.
⚠️ Always choose Body Plain β€” never Body Html. Body Html sends raw HTML code to Claude which produces garbled, broken replies. Body Plain sends clean readable text.
Leave all other fields as default. Click Continue then click Test. Zapier will send your test email to Claude and you will see Claude's reply appear in the test results. Confirm the reply looks like a real, sensible response before moving on.
5
Add Gmail β€” send the reply
Click the + icon below the Claude step. Search for Gmail and select it. For the event choose Send Email and click Continue. Select your Gmail account and click Continue.
πŸ“‹ Setting up each field β€” use the variable picker for every field, do not type email addresses manually:

To field: Click inside the To field, then click the + button. Under Previous Steps click 1. New Email and select From Email. The token {{1. From Email}} will appear β€” this automatically sends the reply to whoever emailed you.

Subject field: Type Re: (with a space after the colon). Then click the + button, select 1. New Email, and choose Subject. The field will show Re: {{1. Subject}} β€” this produces a subject like Re: AI Help.

Body field: Click inside the Body field, then click the + button. Under Previous Steps click 3. Send Message (the Claude step) and select Response Content Text. The token {{3. Response Content Text}} will appear β€” this inserts Claude's reply as the email body.
Leave all other fields blank. Click Continue then click Test. Check your inbox β€” you should receive a real reply email written by Claude within a few seconds.
6
Turn on your agent
Once your test email arrives and the reply looks correct, click Publish in the top right corner of the Zap editor to turn your agent on. It will now run automatically every time you receive an email with AI Help in the subject.
Test it live: Send yourself an email with the subject "AI Help" and a question. Within 30 seconds you should receive an AI-written reply automatically.
πŸ”§ Step 3 β€” Customize and Extend β–Ύ

Now that your basic agent works, try these improvements:

  • Change the trigger β€” instead of Gmail, try a Typeform submission, a Slack message, or a new row in Google Sheets
  • Change the action β€” instead of replying by email, post Claude's response to a Slack channel or save it to a Google Doc
  • Improve the prompt β€” give Claude a persona: "You are a customer support agent for [your business]. Always be friendly, concise, and offer one next step."
  • Add a filter step β€” only trigger the agent if the email contains certain keywords, or only during business hours
  • Add logging β€” add a Google Sheets action that saves every email + Claude's reply so you can review them
2
Beginner
Tool-Use Agent
Learn the most important pattern in agent development β€” the tool-use loop. Build a Python agent with real tools: web search, file writing, and date lookup. Once you understand this loop, every other agent architecture builds on it.
Python Anthropic SDK Tool Use Agent Loop 1–3 hrs
πŸ’‘ What you can build with tool-use agents
Web research bot
File organizer agent
Competitive analysis tool
Daily news summarizer
Auto report generator
SEO keyword researcher
Job listing aggregator
Stock data fetcher
πŸ”“ Skills you'll unlock
Tool definitions The agent loop Processing tool calls Returning tool results Multi-turn conversations
🎯 What You're Building β–Ύ

You'll build a Python agent powered by claude-sonnet-4-6 that can use three real tools: web search via the Serper API, writing files to disk, and getting today's date. The agent decides which tools to call, receives the results, and keeps looping until the task is fully complete. This is the most important pattern in agent development β€” once you understand the tool-use loop, every other agent architecture builds on it.

βš™οΈ Step 1 β€” Set Up and Define Your Tools β–Ύ
1
Create your project folder
Create a new folder, set up a Python virtual environment, and install the required packages.
mkdir tool-agent && cd tool-agent
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install anthropic requests python-dotenv
2
Add your API keys
Create a .env file with your Anthropic and Serper API keys. Get a free Serper key at serper.dev.
ANTHROPIC_API_KEY=your_anthropic_key_here
SERPER_API_KEY=your_serper_key_here
3
Create agent.py with tools and schemas
Define the three tool implementations and their JSON schemas that Claude will use to call them.
import os
import json
import requests
from datetime import date
from dotenv import load_dotenv
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# --- Tool implementations ---

def search_web(query: str) -> str:
    url = "https://google.serper.dev/search"
    headers = {"X-API-KEY": os.getenv("SERPER_API_KEY"), "Content-Type": "application/json"}
    payload = {"q": query, "num": 5}
    response = requests.post(url, json=payload, headers=headers)
    data = response.json()
    results = []
    for item in data.get("organic", []):
        results.append(f"{item['title']}\n{item['link']}\n{item.get('snippet', '')}")
    return "\n\n".join(results) if results else "No results found."

def write_file(filename: str, content: str) -> str:
    with open(filename, "w") as f:
        f.write(content)
    return f"File '{filename}' written successfully ({len(content)} characters)."

def get_date() -> str:
    return f"Today's date is {date.today().strftime('%B %d, %Y')}."

# --- Tool schemas for Claude ---

TOOLS = [
    {
        "name": "search_web",
        "description": "Search the web for current information on any topic. Returns titles, URLs, and snippets.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "The search query to look up."}
            },
            "required": ["query"]
        }
    },
    {
        "name": "write_file",
        "description": "Write text content to a file on disk.",
        "input_schema": {
            "type": "object",
            "properties": {
                "filename": {"type": "string", "description": "The name of the file to write, e.g. 'report.txt'."},
                "content": {"type": "string", "description": "The text content to write into the file."}
            },
            "required": ["filename", "content"]
        }
    },
    {
        "name": "get_date",
        "description": "Returns today's current date.",
        "input_schema": {
            "type": "object",
            "properties": {},
            "required": []
        }
    }
]
πŸ” Step 2 β€” Build the Agent Loop β–Ύ
1
Add the run_agent function
The agent loop sends messages to Claude, processes any tool calls it makes, appends results, and repeats until Claude signals end_turn.
def run_tool(name: str, tool_input: dict) -> str:
    if name == "search_web":
        return search_web(tool_input["query"])
    elif name == "write_file":
        return write_file(tool_input["filename"], tool_input["content"])
    elif name == "get_date":
        return get_date()
    return "Unknown tool."

def run_agent(task: str):
    print(f"\nTask: {task}\n{'='*50}")
    messages = [{"role": "user", "content": task}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            tools=TOOLS,
            messages=messages
        )

        # Append Claude's response to message history
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            # Extract final text response
            for block in response.content:
                if hasattr(block, "text"):
                    print(f"\nAgent: {block.text}")
            break

        # Process tool calls
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                print(f"  [Tool] {block.name}({json.dumps(block.input)})")
                result = run_tool(block.name, block.input)
                print(f"  [Result] {result[:200]}{'...' if len(result) > 200 else ''}\n")
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result
                })

        # Append tool results and loop again
        messages.append({"role": "user", "content": tool_results})

if __name__ == "__main__":
    run_agent(
        "Search for the latest Claude API updates, summarize the key points, "
        "and save the summary to report.txt. Include today's date at the top."
    )
2
Run the agent
Change the task string in __main__ to any research question, then run the script. You should see tool calls logged as the agent works.
python agent.py
3
Intermediate
Extended Thinking Agent
Use Claude's extended thinking feature to build a deep analysis agent. Claude reasons step-by-step through complex problems before answering β€” you'll see its visible thinking blocks and control how deeply it reasons.
Python claude-opus-4-6 Extended Thinking Deep Analysis 2–4 hrs
πŸ’‘ What you can build with extended thinking agents
Business plan analyzer
Code architecture reviewer
Legal document analyzer
Investment thesis evaluator
Technical spec reviewer
Risk assessment agent
Strategy evaluator
Research synthesizer
πŸ”“ Skills you'll unlock
Extended thinking API Thinking blocks Budget tokens When to use thinking Complex analysis tasks
🧠 What Is Extended Thinking? β–Ύ

Extended thinking lets Claude reason through a problem step by step before committing to an answer. You set a budget_tokens limit, and Claude uses that token budget to think silently β€” exploring angles, checking its logic, reconsidering β€” before producing a final response.

The result is dramatically better performance on: complex multi-step analysis, ambiguous decisions with trade-offs, long documents with subtle implications, and tasks requiring structured reasoning. Use claude-opus-4-6 for the best thinking quality.

When not to use it: simple factual questions, creative writing, or tasks where latency matters more than depth. Extended thinking takes longer and costs more tokens β€” save it for problems that deserve it.

🎯 What You're Building β–Ύ

A document analysis agent that reads a business problem or text file and produces a deep, structured analysis using Claude's extended thinking. You'll see the raw reasoning blocks as Claude works through the problem, then the polished final answer. The CLI accepts a file path as an argument so you can analyze any document instantly.

βš™οΈ Step 1 β€” Enable Extended Thinking β–Ύ
1
Install the Anthropic SDK
Install the required packages in your project.
pip install anthropic python-dotenv
2
Create thinking_demo.py
This minimal script demonstrates extended thinking. Run it first to confirm the API is working and to see Claude's reasoning blocks.
import os
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

prompt = "A startup has $500k runway, 6 months to profitability, but a competitor just raised $10M. Should they pivot, double down, or seek funding? Analyze carefully."

response = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=16000,
    thinking={"type": "enabled", "budget_tokens": 10000},
    messages=[{"role": "user", "content": prompt}]
)

for block in response.content:
    if block.type == "thinking":
        print("=== Claude's Reasoning ===")
        print(block.thinking)
        print()
    elif block.type == "text":
        print("=== Final Answer ===")
        print(block.text)
3
Run the demo
Watch Claude's internal reasoning appear before the answer. Notice how it explores multiple angles before concluding.
python thinking_demo.py
πŸ”¨ Step 2 β€” Build the Analysis Agent β–Ύ
1
Create analyze.py
The full agent reads any text file, lets you pick analysis depth (quick/normal/deep), and saves structured findings to a new file.
import os
import sys
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# Budget presets β€” tune based on complexity
DEPTH_BUDGETS = {
    "quick":  4000,   # Simple questions, short docs
    "normal": 10000,  # Most business analysis tasks
    "deep":   20000,  # Complex contracts, multi-factor decisions
}

SYSTEM_PROMPT = """You are a senior business analyst and strategic advisor.
When analyzing documents or problems, structure your response with:
1. Executive Summary (2-3 sentences)
2. Key Findings (bullet points)
3. Risks and Concerns
4. Recommendations (prioritized)
5. Open Questions

Be direct, specific, and actionable."""

def analyze(file_path: str, depth: str = "normal", question: str = None):
    if not os.path.exists(file_path):
        print(f"Error: File '{file_path}' not found.")
        sys.exit(1)

    with open(file_path, "r") as f:
        document = f.read()

    budget = DEPTH_BUDGETS.get(depth, DEPTH_BUDGETS["normal"])

    user_message = f"Document to analyze:\n\n{document}"
    if question:
        user_message += f"\n\nSpecific question: {question}"
    else:
        user_message += "\n\nProvide a comprehensive analysis of this document."

    print(f"Analyzing '{file_path}' at depth='{depth}' (budget_tokens={budget})...")
    print("Thinking...\n")

    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=16000,
        thinking={"type": "enabled", "budget_tokens": budget},
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": user_message}]
    )

    thinking_shown = False
    for block in response.content:
        if block.type == "thinking":
            if not thinking_shown:
                print("=== Internal Reasoning (thinking blocks) ===")
                thinking_shown = True
            # Show a preview β€” full thinking can be very long
            preview = block.thinking[:1000]
            print(preview + ("..." if len(block.thinking) > 1000 else ""))
            print()
        elif block.type == "text":
            print("=== Analysis ===")
            print(block.text)

    # Save output
    out_path = file_path.replace(".txt", "") + "_analysis.txt"
    final_text = next((b.text for b in response.content if b.type == "text"), "")
    with open(out_path, "w") as f:
        f.write(final_text)
    print(f"\nAnalysis saved to: {out_path}")

if __name__ == "__main__":
    # Usage: python analyze.py  [depth] [question]
    if len(sys.argv) < 2:
        print("Usage: python analyze.py  [quick|normal|deep] [optional question]")
        sys.exit(1)

    file_path = sys.argv[1]
    depth = sys.argv[2] if len(sys.argv) > 2 else "normal"
    question = sys.argv[3] if len(sys.argv) > 3 else None

    analyze(file_path, depth, question)
2
Create a test document
Create a sample business plan to test your agent with.
# Create a quick test document
cat > business_plan.txt << 'EOF'
Company: NovaMesh
Product: AI-powered mesh WiFi that auto-optimizes for each household's usage patterns.
Stage: Pre-seed. $180k raised from angels.
Team: 2 engineers, 1 designer. No sales hire yet.
Traction: 120 beta users, 4.6/5 rating, 0% churn in 3 months.
Market: $12B home networking market growing 8% YoY.
Ask: $1.2M seed to hire sales lead, scale manufacturing, launch in 3 cities.
Competition: Eero (Amazon), Google Nest WiFi, Orbi.
Concern: Manufacturing lead times are 16 weeks, limiting rapid iteration.
EOF
3
Run at different depths
Test quick and deep analysis. The deep mode uses more tokens and takes longer but produces significantly more thorough analysis.
# Quick analysis
python analyze.py business_plan.txt quick

# Deep analysis with a specific question
python analyze.py business_plan.txt deep "Is the $1.2M ask reasonable given their traction?"
4
Advanced
Multi-Agent Orchestration
Build three specialized agents β€” Researcher, Writer, Reviewer β€” that pass structured JSON to each other. The pipeline retries automatically when the reviewer scores too low. This is how production AI systems are built.
Python claude-sonnet-4-6 Structured Outputs Orchestration 3–6 hrs
πŸ’‘ What you can build with multi-agent pipelines
Blog post factory
Marketing copy pipeline
Research β†’ report pipeline
Product description writer
Email campaign creator
Technical doc generator
Proposal writer
Content QA system
πŸ”“ Skills you'll unlock
Agent specialization Structured JSON outputs Quality gates Retry logic Parallel execution
🎯 What You're Building β–Ύ

A three-agent pipeline where each agent is a specialist: the Researcher gathers and structures key points, the Writer turns research into a polished article, and the Reviewer scores the article 1–10. If the score is below 7, the Writer gets a second chance with the Reviewer's specific feedback. Agents communicate through structured JSON β€” not free text β€” so outputs are reliable and machine-readable.

βš™οΈ Step 1 β€” Build Agents with Structured Outputs β–Ύ
1
Create pipeline.py with the three agents
Each agent is a Python function returning structured JSON. We parse and validate the response before passing it downstream.
import os
import json
import re
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"

def call_claude(system: str, user: str) -> str:
    response = client.messages.create(
        model=MODEL,
        max_tokens=4096,
        system=system,
        messages=[{"role": "user", "content": user}]
    )
    return response.content[0].text

def extract_json(text: str) -> dict:
    """Strip markdown fences and parse JSON from Claude's response."""
    cleaned = re.sub(r"```(?:json)?", "", text).strip().strip("`").strip()
    return json.loads(cleaned)

# --- Agent 1: Researcher ---

def research_agent(topic: str) -> dict:
    system = """You are a research specialist. Your ONLY output is valid JSON β€” no explanation, no markdown prose.
Return this exact structure:
{
  "key_points": ["point 1", "point 2", "point 3", "point 4", "point 5"],
  "context": "2-3 sentences of background context",
  "angle": "The most compelling angle or hook for this topic"
}"""
    user = f"Research this topic thoroughly: {topic}"
    raw = call_claude(system, user)
    return extract_json(raw)

# --- Agent 2: Writer ---

def writer_agent(topic: str, research: dict, feedback: str = None) -> str:
    system = """You are an expert content writer. Write clear, engaging articles for a general audience.
Structure: compelling headline, intro hook, 3-4 substantive paragraphs, strong conclusion.
Return ONLY the article text β€” no JSON, no commentary."""
    user = f"""Topic: {topic}

Research to use:
Key points: {json.dumps(research['key_points'], indent=2)}
Context: {research['context']}
Angle: {research['angle']}"""
    if feedback:
        user += f"\n\nPrevious version was rejected. Reviewer feedback to address:\n{feedback}"
    return call_claude(system, user)

# --- Agent 3: Reviewer ---

def reviewer_agent(topic: str, article: str) -> dict:
    system = """You are a senior editorial reviewer. Evaluate articles rigorously. Your ONLY output is valid JSON.
Return this exact structure:
{
  "score": 7,
  "issues": ["issue 1", "issue 2"],
  "verdict": "pass",
  "improvements": "Specific, actionable instructions for the writer to improve the article."
}
score: integer 1-10. verdict: "pass" if score >= 7, "fail" if below 7."""
    user = f"Topic: {topic}\n\nArticle to review:\n\n{article}"
    raw = call_claude(system, user)
    return extract_json(raw)
2
Test each agent individually
Before wiring agents together, verify each one works in isolation.
# Quick smoke test β€” add to the bottom of pipeline.py temporarily
if __name__ == "__main__":
    topic = "How large language models are changing software development"
    print("Testing research agent...")
    research = research_agent(topic)
    print(json.dumps(research, indent=2))

    print("\nTesting writer agent...")
    article = writer_agent(topic, research)
    print(article[:500])

    print("\nTesting reviewer agent...")
    review = reviewer_agent(topic, article)
    print(json.dumps(review, indent=2))
πŸ” Step 2 β€” Orchestrate with Quality Gates β–Ύ
1
Add the run_pipeline orchestrator
The orchestrator runs the full pipeline: Research β†’ Write β†’ Review. If the score is below 7, the Writer gets feedback and tries again β€” up to 3 times.
def run_pipeline(topic: str, max_retries: int = 3):
    print(f"\n{'='*60}")
    print(f"Pipeline starting: {topic}")
    print(f"{'='*60}\n")

    # Stage 1: Research
    print("[1/3] Research Agent working...")
    research = research_agent(topic)
    print(f"      Found {len(research['key_points'])} key points. Angle: {research['angle'][:80]}...")

    # Stage 2 & 3: Write β†’ Review β†’ Retry loop
    article = None
    feedback = None

    for attempt in range(1, max_retries + 1):
        print(f"\n[2/3] Writer Agent β€” attempt {attempt}/{max_retries}...")
        article = writer_agent(topic, research, feedback)
        print(f"      Article written ({len(article)} characters).")

        print(f"[3/3] Reviewer Agent scoring...")
        review = reviewer_agent(topic, article)
        score = review["score"]
        verdict = review["verdict"]

        print(f"      Score: {score}/10 β€” Verdict: {verdict.upper()}")

        if verdict == "pass":
            print(f"\nβœ“ Pipeline complete on attempt {attempt}!")
            break
        else:
            if attempt < max_retries:
                print(f"      Issues: {', '.join(review['issues'])}")
                print(f"      Sending feedback to writer for revision...")
                feedback = review["improvements"]
            else:
                print(f"\n  Max retries reached. Saving best version (score: {score}/10).")

    # Save final article
    safe_name = topic[:40].replace(" ", "_").replace("/", "-")
    out_file = f"{safe_name}_article.txt"
    with open(out_file, "w") as f:
        f.write(f"Topic: {topic}\n")
        f.write(f"Final score: {review['score']}/10\n")
        f.write("="*60 + "\n\n")
        f.write(article)

    print(f"\nFinal article saved to: {out_file}")
    return article, review

if __name__ == "__main__":
    run_pipeline("How large language models are changing software development")
2
Run the full pipeline
Watch all three agents collaborate. Try nuanced topics where the Reviewer is more likely to reject the first draft, so you see the retry loop in action.
python pipeline.py
5
Pro
MCP: Claude Connected to Everything
Configure built-in MCP servers to give Claude access to your filesystem, Supabase database, and GitHub. Then build your own custom MCP server in Python that exposes any tool you want.
Claude Code MCP Python Supabase 4–7 hrs
πŸ’‘ What you can build with MCP agents
Database query agent
File management assistant
GitHub PR reviewer
Supabase data explorer
Multi-tool research agent
Code review bot
Custom API connector
Local file search agent
πŸ”“ Skills you'll unlock
MCP protocol Configuring MCP servers Filesystem MCP Supabase MCP Building a custom MCP server
🧠 What Is MCP? β–Ύ

MCP (Model Context Protocol) is an open standard created by Anthropic that lets Claude connect to external tools and data sources through a consistent interface. Instead of writing custom integration code for every service, you configure MCP servers once and Claude can use them automatically β€” in Claude Code, Claude Desktop, or any MCP-compatible client.

There are ready-made MCP servers for: filesystem access, Supabase databases, GitHub repositories, Slack, Notion, browser automation, and dozens more. You can also build your own in Python or TypeScript in minutes.

Think of it as a USB standard for AI tools. One protocol, unlimited integrations.

🎯 What You're Building β–Ύ

You'll connect Claude Code to three MCP servers: the built-in filesystem server, the official Supabase server, and a custom Python server you build yourself. By the end, you'll be able to ask Claude Code in plain English to read your files, query your database, save notes, and check the weather β€” all through natural conversation.

βš™οΈ Step 1 β€” Configure Built-In MCP Servers β–Ύ
1
Open Claude Code MCP settings
Claude Code manages MCP servers through its settings. Open the MCP configuration panel with this command.
claude mcp
2
Add filesystem and Supabase MCP servers to your config
Add both servers to your Claude Code config. Replace the path and token with your own values. Get your Supabase access token at app.supabase.com β†’ Account β†’ Access Tokens.
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/path/to/your/projects"
      ]
    },
    "supabase": {
      "command": "npx",
      "args": [
        "-y",
        "@supabase/mcp-server-supabase@latest",
        "--access-token",
        "YOUR_SUPABASE_ACCESS_TOKEN"
      ]
    }
  }
}
3
Test the filesystem and Supabase servers in Claude Code
Test that both servers are working by asking Claude Code these questions in a new session.
# Ask Claude Code these prompts (no code needed β€” just type them):

"List the files in my projects folder"

"Show me all the tables in my Supabase database"

"Read the contents of [any file in your projects folder]"
πŸ”¨ Step 2 β€” Build a Custom MCP Server β–Ύ
1
Install the mcp library
Install the required packages. The mcp library handles all the protocol details.
pip install mcp requests
2
Create server.py β€” your custom MCP server
Build your own MCP server in Python that exposes two tools: a weather lookup and a note saver.
import json
import os
import requests
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

app = Server("my-tools")

@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="get_weather",
            description="Get the current weather for a city.",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "The city name, e.g. 'London'"}
                },
                "required": ["city"]
            }
        ),
        types.Tool(
            name="save_note",
            description="Save a note to a local file.",
            inputSchema={
                "type": "object",
                "properties": {
                    "title": {"type": "string", "description": "The note title (used as filename)."},
                    "content": {"type": "string", "description": "The note content."}
                },
                "required": ["title", "content"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "get_weather":
        city = arguments["city"]
        # Using open-meteo (free, no API key required) via geocoding
        geo = requests.get(
            f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1"
        ).json()
        if not geo.get("results"):
            return [types.TextContent(type="text", text=f"City '{city}' not found.")]
        loc = geo["results"][0]
        weather = requests.get(
            f"https://api.open-meteo.com/v1/forecast"
            f"?latitude={loc['latitude']}&longitude={loc['longitude']}"
            f"¤t_weather=true"
        ).json()
        cw = weather["current_weather"]
        result = f"Weather in {city}: {cw['temperature']}Β°C, wind {cw['windspeed']} km/h"
        return [types.TextContent(type="text", text=result)]

    elif name == "save_note":
        title = arguments["title"]
        content = arguments["content"]
        notes_dir = os.path.expanduser("~/mcp-notes")
        os.makedirs(notes_dir, exist_ok=True)
        safe_title = title.replace(" ", "_").replace("/", "-")
        file_path = os.path.join(notes_dir, f"{safe_title}.txt")
        with open(file_path, "w") as f:
            f.write(f"# {title}\n\n{content}")
        return [types.TextContent(type="text", text=f"Note saved to {file_path}")]

    return [types.TextContent(type="text", text="Unknown tool.")]

if __name__ == "__main__":
    import asyncio
    asyncio.run(stdio_server(app))
3
Register the custom server in your MCP config
Add it alongside the other servers you already configured.
{
  "mcpServers": {
    "filesystem": { "...": "..." },
    "supabase": { "...": "..." },
    "my-tools": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}
4
Restart Claude Code and test your custom tools
Open a new Claude Code session and ask it to use your tools.
# Ask Claude Code:

"What's the weather in Tokyo right now?"

"Save a note titled 'Project Ideas' with the content: Build a Claude agent that monitors my inbox"

# Then verify:
ls ~/mcp-notes/
6
Ninja
Production Agent + Computer Use
Use Claude's computer use API to build an agent that browses the web autonomously. Then deploy a production agent to Railway with Docker, cron scheduling, and live logs. Your agent runs 24/7 with zero server management.
Python Computer Use API Railway Docker 5–9 hrs
πŸ’‘ What you can build with production agents
Daily market monitor
Competitor price tracker
Lead generation bot
News aggregator
Job listing monitor
Social media tracker
E-commerce price watcher
Scheduled report sender
πŸ”“ Skills you'll unlock
Computer use API Screenshot β†’ action loop Production deployment Docker Environment variables Monitoring
🧠 What Is Computer Use? β–Ύ

Claude's computer use capability lets it see screenshots and control a computer β€” clicking at coordinates, typing text, pressing keys, and scrolling. The loop is simple: you send a screenshot, Claude decides what action to take, you execute that action, send the next screenshot, repeat.

Real-world use cases: browser automation that requires JavaScript rendering, filling out complex web forms, UI testing across different environments, and scraping websites that block traditional scrapers.

In this project we use Playwright instead of raw screenshot + mouse control. Playwright runs a real Chromium browser, gives Claude actual screenshots, and executes its actions safely β€” no risk of Claude accidentally clicking something on your real desktop.

🎯 What You're Building β–Ύ

Part 1: A computer use agent that opens any URL in a real browser, takes a screenshot, and uses Claude to extract specific information from the page β€” handling JavaScript-rendered content that regular scrapers can't touch.

Part 2: Package a production tool-use agent into Docker and deploy it to Railway with environment variable management, a cron schedule for automated runs, and log monitoring.

βš™οΈ Step 1 β€” Computer Use Agent with Playwright β–Ύ
1
Install dependencies
Install Playwright and the Anthropic SDK. Playwright manages a real Chromium browser, so Claude sees exactly what a human would see.
pip install anthropic playwright python-dotenv
python -m playwright install chromium
2
Create web_agent.py with the screenshot β†’ action loop
This agent opens a URL in a real browser, sends screenshots to Claude, and executes whatever actions Claude requests until the task is complete.
import os
import base64
import json
from dotenv import load_dotenv
import anthropic
from playwright.sync_api import sync_playwright

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

def screenshot_to_base64(page) -> str:
    """Take a screenshot of the current page and return as base64."""
    img_bytes = page.screenshot(full_page=False)
    return base64.standard_b64encode(img_bytes).decode("utf-8")

def run_computer_use_agent(url: str, task: str, max_steps: int = 10):
    print(f"\nTask: {task}")
    print(f"URL:  {url}\n")

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page(viewport={"width": 1280, "height": 800})
        page.goto(url, wait_until="networkidle", timeout=30000)
        print("Page loaded. Starting agent loop...")

        messages = []
        tools = [
            {
                "type": "computer_20250124",
                "name": "computer",
                "display_width_px": 1280,
                "display_height_px": 800,
            }
        ]

        # Initial screenshot
        screenshot_b64 = screenshot_to_base64(page)
        messages.append({
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {"type": "base64", "media_type": "image/png", "data": screenshot_b64}
                },
                {"type": "text", "text": f"Task: {task}\n\nHere is the current state of the browser. Complete the task. When done, use the text tool to return your final answer."}
            ]
        })

        for step in range(max_steps):
            print(f"Step {step + 1}/{max_steps}...")
            response = client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=4096,
                tools=tools,
                messages=messages,
                betas=["computer-use-2025-01-24"],
            )

            messages.append({"role": "assistant", "content": response.content})

            if response.stop_reason == "end_turn":
                for block in response.content:
                    if hasattr(block, "text") and block.text:
                        print(f"\nResult:\n{block.text}")
                        browser.close()
                        return block.text
                break

            # Execute computer use actions
            tool_results = []
            for block in response.content:
                if block.type == "tool_use" and block.name == "computer":
                    action = block.input.get("action")
                    print(f"  Action: {action} {json.dumps({k:v for k,v in block.input.items() if k != 'action'})}")

                    if action == "screenshot":
                        img = screenshot_to_base64(page)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": [{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": img}}]
                        })
                    elif action == "left_click":
                        page.mouse.click(block.input["coordinate"][0], block.input["coordinate"][1])
                        page.wait_for_timeout(500)
                        img = screenshot_to_base64(page)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": [{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": img}}]
                        })
                    elif action == "type":
                        page.keyboard.type(block.input["text"])
                        tool_results.append({"type": "tool_result", "tool_use_id": block.id, "content": "Typed successfully."})
                    elif action == "key":
                        page.keyboard.press(block.input["key"])
                        tool_results.append({"type": "tool_result", "tool_use_id": block.id, "content": "Key pressed."})
                    elif action == "scroll":
                        page.mouse.wheel(0, block.input.get("delta_y", 300))
                        tool_results.append({"type": "tool_result", "tool_use_id": block.id, "content": "Scrolled."})

            if tool_results:
                messages.append({"role": "user", "content": tool_results})

        browser.close()
        return "Max steps reached."

if __name__ == "__main__":
    result = run_computer_use_agent(
        url="https://news.ycombinator.com",
        task="Find the top 3 story titles on the front page and return them as a numbered list."
    )
    print(f"\nFinal answer: {result}")
3
Run the web agent and verify it extracts page content
Try changing the URL and task. Good test: point it at a JavaScript-heavy site like a job board or pricing page and ask it to extract structured data.
python web_agent.py
πŸš€ Step 2 β€” Deploy to Railway with Docker β–Ύ

Package the tool-use agent from Project 2 (or any agent) into a Docker container and deploy it to Railway. Railway auto-detects Dockerfiles, manages environment variables, and gives you logs and cron scheduling.

1
Create requirements.txt
List all your agent's dependencies so Docker can install them during the build.
anthropic>=0.40.0
requests>=2.31.0
python-dotenv>=1.0.0
2
Create Dockerfile
This Dockerfile builds a minimal Python container with your agent code installed and ready to run.
FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy agent code
COPY agent.py .

# Run the agent
CMD ["python", "agent.py"]
3
Update agent.py to read task from environment variable
Create a production-ready version of your agent that reads its task from an environment variable. This lets Railway pass different tasks via config without changing code.
# Add to the bottom of agent.py, replacing the existing __main__ block:
if __name__ == "__main__":
    import logging
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s"
    )

    task = os.getenv(
        "AGENT_TASK",
        "Search for today's top AI news, summarize 3 stories, and save to daily_brief.txt"
    )
    logging.info(f"Starting agent with task: {task}")
    run_agent(task)
    logging.info("Agent completed successfully.")
4
Deploy to Railway
Install the Railway CLI, log in, initialize a project, and deploy. Railway detects your Dockerfile automatically.
# Install Railway CLI
npm install -g @railway/cli

# Login and initialize
railway login
railway init

# Deploy (Railway detects your Dockerfile automatically)
railway up
5
Set environment variables in Railway dashboard
In the Railway dashboard, go to your service β†’ Variables and add these environment variables.
ANTHROPIC_API_KEY=your_key_here
SERPER_API_KEY=your_key_here
AGENT_TASK=Search for the latest AI research papers, summarize the top 3, save to report.txt
6
Configure cron schedule and verify logs in Railway dashboard
In Railway: go to your service β†’ Settings β†’ Cron Schedule. Set a schedule and then tail your logs to confirm the agent is running correctly. Any push to your connected GitHub repo triggers an automatic redeploy.
# Example cron expressions for Railway:
0 8 * * *      # Every day at 8am UTC
0 */6 * * *    # Every 6 hours
0 9 * * 1-5   # Weekdays at 9am UTC

# View live logs from CLI:
railway logs --tail