BD Brain Drip
Build a LangGraph Agent Team

Step 9: Run Locally

Build the CLI entry point, run the complete pipeline on a real job posting, and inspect the output.

Prerequisites | Complete graph from Step 8

Build the Entry Point

Create main.py β€” the command-line interface for your agent team:

# main.py
# ==========================================
# CLI entry point for the job application agent
# ==========================================
# Usage:
#   python main.py --job job_posting.txt --resume resume.md
#   python main.py --job-text "paste job posting here" --resume resume.md

import argparse
import sys
import time
from graph import app
from utils import load_file


def main():
    parser = argparse.ArgumentParser(
        description="AI-powered job application assistant"
    )
    parser.add_argument(
        "--job",
        help="Path to a text file containing the job posting",
    )
    parser.add_argument(
        "--job-text",
        help="Job posting text (paste directly)",
    )
    parser.add_argument(
        "--resume",
        required=True,
        help="Path to your resume (Markdown format)",
    )
    args = parser.parse_args()

    # Load the job posting
    if args.job:
        job_posting = load_file(args.job)
    elif args.job_text:
        job_posting = args.job_text
    else:
        print("Error: Provide either --job (file path) or --job-text (raw text)")
        sys.exit(1)

    # Load the resume
    resume = load_file(args.resume)

    # Run the pipeline
    print("\nπŸ” Job Application Agent")
    print("=" * 50)

    start = time.time()

    print("\n[1/4] Analyzing job posting...")
    result = app.invoke({
        "job_posting": job_posting,
        "resume": resume,
    })

    elapsed = time.time() - start

    # Print summary
    print("\n" + "=" * 50)
    print("πŸ“‹ Summary")
    print("=" * 50)
    print(f"Company:  {result.get('company_name', 'Unknown')}")
    print(f"Role:     {result.get('job_title', 'Unknown')}")
    print(f"Review:   {'βœ… PASSED' if result.get('review_passed') else '⚠️  NEEDS WORK'}")
    print(f"Time:     {elapsed:.1f}s")
    print(f"\nπŸ“ Output files saved to ./output/")
    print("   - tailored_resume.md")
    print("   - cover_letter.md")
    print("   - review.md")


if __name__ == "__main__":
    main()

Create a Test Job Posting

Save a realistic job posting to test with:

<!-- job_posting.txt -->
Senior Backend Engineer β€” Notion

About Notion:
Notion is the connected workspace where better, faster work happens.
We believe everyone should be able to organize their work and life β€”
and that tools should adapt to people, not the other way around.

About the Role:
We are looking for a Senior Backend Engineer to work on our core
infrastructure team. You will build and maintain the systems that
power Notion's real-time collaboration features, serving millions
of users worldwide.

What You'll Do:
- Design and implement scalable backend services in Python and Go
- Build real-time collaboration infrastructure (CRDTs, WebSockets)
- Optimize database performance for high-throughput workloads
- Work with PostgreSQL, Redis, and Kafka at scale
- Contribute to system architecture decisions
- Mentor engineers and review code

What We're Looking For:
- 5+ years of backend engineering experience
- Strong proficiency in Python (Go experience is a plus)
- Experience building real-time or collaborative systems
- Deep knowledge of PostgreSQL and distributed databases
- Experience with message queues (Kafka, RabbitMQ)
- Familiarity with Docker, Kubernetes, and cloud infrastructure (AWS)
- Excellent written and verbal communication skills
- B.S. or M.S. in Computer Science (or equivalent experience)

Nice to Have:
- Experience with CRDTs or operational transform
- Contributions to open-source projects
- Experience at a high-growth startup

Compensation: $190k-$250k + equity
Location: San Francisco (hybrid) or Remote (US)

Save this as job_posting.txt in your project root.

Run the Pipeline

python main.py --job job_posting.txt --resume sample_resume.md

You should see output like:

πŸ” Job Application Agent
==================================================

[1/4] Analyzing job posting...
  Saved: output/tailored_resume.md
  Saved: output/cover_letter.md
  Saved: output/review.md

==================================================
πŸ“‹ Summary
==================================================
Company:  Notion
Role:     Senior Backend Engineer
Review:   βœ… PASSED
Time:     28.3s

πŸ“ Output files saved to ./output/
   - tailored_resume.md
   - cover_letter.md
   - review.md

Inspect the Output

Open each file and check the quality:

# View the tailored resume
cat output/tailored_resume.md

# View the cover letter
cat output/cover_letter.md

# View the review
cat output/review.md

What to Look For

In the tailored resume:

  • Keywords from the job posting woven into bullet points (Python, PostgreSQL, Redis, Kafka)
  • Experience reframed for backend/infrastructure focus
  • Relevant skills listed first

In the cover letter:

  • Specific mention of Notion (not generic)
  • 2-3 experience examples mapped to job requirements
  • Reference to Notion’s culture or values
  • Under 400 words

In the review:

  • Numeric scores for each quality dimension
  • Specific strengths and improvement suggestions
  • PASS or NEEDS_REVISION verdict

Adding Streaming Output

For a better user experience, you can add progress callbacks. Update main.py to show which agent is running:

# Alternative: Run with step-by-step output using stream
def run_with_streaming(job_posting: str, resume: str):
    """Run the pipeline with visible progress for each step."""
    agent_names = {
        "analyze": "[1/4] Analyzing job posting",
        "tailor": "[2/4] Tailoring resume",
        "writer": "[2/4] Writing cover letter",
        "review": "[3/4] Reviewing application",
        "save": "[4/4] Saving outputs",
    }

    for event in app.stream(
        {"job_posting": job_posting, "resume": resume},
        stream_mode="updates",
    ):
        for node_name in event:
            label = agent_names.get(node_name, node_name)
            print(f"{label}... done")

The stream() method yields events as each node completes, so you can show real-time progress.

Your Final Project Structure

job-application-agent/
β”œβ”€β”€ agents/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ analyzer.py       βœ… Job Analyzer
β”‚   β”œβ”€β”€ tailor.py         βœ… Resume Tailor
β”‚   β”œβ”€β”€ writer.py         βœ… Cover Letter Writer
β”‚   └── reviewer.py       βœ… Application Reviewer
β”œβ”€β”€ output/
β”‚   β”œβ”€β”€ tailored_resume.md  βœ… Generated
β”‚   β”œβ”€β”€ cover_letter.md     βœ… Generated
β”‚   └── review.md           βœ… Generated
β”œβ”€β”€ state.py              βœ… Shared state definition
β”œβ”€β”€ graph.py              βœ… LangGraph workflow
β”œβ”€β”€ utils.py              βœ… Configuration and helpers
β”œβ”€β”€ main.py               βœ… CLI entry point
β”œβ”€β”€ sample_resume.md      βœ… Test resume
β”œβ”€β”€ job_posting.txt       βœ… Test job posting
β”œβ”€β”€ .env                  βœ… API key
└── requirements.txt      βœ… Dependencies

Reference: LangGraph Streaming Β· LangGraph How-To Guides

← Wire the Graph | Next: Step 10 - What’s Next β†’