Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Anchor

5-workflow n8n orchestration system — turns a job-posting URL into a factually-grounded application packet with human-in-loop review gates.


The architectural insight

The default failure mode of AI-generated resumes is hallucinated experience — technologies you never used, "contributed to" inflated into "led," invented metrics. Anchor prevents this at the schema level, not the prompt level: the master resume is stored as typed, addressable rows in Postgres (master_resume_entry) rather than a text blob. Tailoring a resume is selection and rephrasing of those rows, not free-form generation. A Grounding Critic agent then checks every output line against its cited source entry before anything ships — new technologies, inflated numbers, or stronger role titles than the source supports all fail the check.

The same architectural pattern that makes Meridian's citations traceable makes Anchor's generated materials factually grounded — by construction, not by instruction.

What it does

Paste a LinkedIn/Greenhouse/Lever/Workday URL into the dashboard. The pipeline scrapes the JD, researches the company from 4 parallel sources, scores your match against the JD, drafts a tailored resume + cover letter + LinkedIn message, generates a skill-gap report, renders PDFs, uploads to Google Drive, and creates a Notion page — all automatically, with a Slack gate if the match score is weak enough that a human should decide whether to continue.

System diagram

flowchart TD
    USER["Browser: paste job URL"] -->|POST /webhook/intake| WF1

    subgraph WF1["Workflow 1 — Job Intake (7 nodes)"]
        V[Validate URL] --> INS[Insert application row]
        INS --> RESP["Respond < 500ms"]
    end

    WF1 -->|Execute Workflow| WF2

    subgraph WF2["Workflow 2 — Job Processor (25 nodes)"]
        FETCH[Playwright: fetch JD page] --> PARSE[JD Parser Agent]
        PARSE --> RESEARCH["4 parallel branches:\nnews / homepage / about / careers"]
        RESEARCH --> SYNTH[Company Synthesizer Agent]
        SYNTH --> UPSERT["Upsert company + status → researched"]
    end

    WF2 -->|Execute Workflow| WF3

    subgraph WF3["Workflow 3 — Match & Generate (67 nodes)"]
        CRITIC[Resume Critic Agent] --> SCORER[Match Scorer Agent]
        SCORER -->|"score < 60"| SLACK_GATE["Slack: Continue/Skip?"]
        SCORER -->|"score ≥ 60"| TAILOR[Resume Tailorer Agent]
        TAILOR --> GROUND[Grounding Critic Agent]
        GROUND -->|pass| MATERIALS["Cover Letter + LinkedIn\n+ Skill Gap agents"]
        GROUND -->|"fail (retry once)"| TAILOR
        GROUND -->|"fail twice"| ESCALATE[Slack: escalation alert]
        MATERIALS --> PDF[PDF render via Playwright]
        PDF --> DRIVE[Upload to Google Drive]
        DRIVE --> NOTION[Create Notion page]
        NOTION --> DONE["status → awaiting_review"]
    end

    subgraph WF4["Workflow 4 — Follow-up Scheduler (14 nodes)"]
        CRON4["Cron: daily 8am"] --> FIND[Find due applications]
        FIND --> DECIDE[Follow-up Decision Agent]
        DECIDE --> DIGEST4[Slack digest]
    end

    subgraph WF5["Workflow 5 — Weekly Reflection (8 nodes)"]
        CRON5["Cron: Sunday 7pm"] --> AGG[Aggregate last 4 weeks]
        AGG --> PATTERN["Pattern Detector Agent\n(min N=5 guard)"]
        PATTERN --> DIGEST5[Slack digest]
    end

    subgraph ERR["Error Workflow (6 nodes)"]
        ETRIG[n8n error trigger] --> RETRY["Retry (max 2, backoff)"]
        RETRY --> ALERT[Slack alert]
    end

    WF1 -.->|errorWorkflow| ERR
    WF2 -.->|errorWorkflow| ERR
    WF3 -.->|errorWorkflow| ERR

    PG[(Postgres\n10 tables)] --- WF1
    PG --- WF2
    PG --- WF3
    PG --- WF4
    PG --- WF5
    LLM["Ollama qwen2.5:7b\nvia FastAPI wrapper"] --- WF2
    LLM --- WF3
    LLM --- WF4
    LLM --- WF5
Loading

Dashboard — Next.js 14, direct Postgres queries, JWT auth:

Application Kanban Application Detail
Kanban Detail

More canvases: Error Handler · Job Intake · Job Processor · Match & Generate · Follow-up Scheduler · Weekly Reflection

The five workflows

Job Intake exists to answer under 500ms so the browser never hangs on a slow scrape — it validates the URL, inserts a row, and fires the actual processing (Workflow 2) asynchronously. That split between "acknowledge fast" and "do the slow work" is why this is a separate workflow rather than one step in Workflow 2.

Job Processor decides what the company actually is before anything gets scored — it scrapes the JD, parses it into structured fields, then researches the company from 4 independent branches (news, homepage, about page, careers page) in parallel, each failure-tolerant so one dead link doesn't kill the run. A synthesizer agent merges those branches into one company profile before handing off.

Match & Generate is where the grounding constraint actually gets enforced. A critic scores fit against the JD; below 60 the pipeline pauses for a human Slack decision rather than guessing whether it's worth tailoring. Above that, the tailorer drafts a resume from the structured entries, the Grounding Critic checks it line-by-line against cited sources, and a failure triggers one retry with the violations fed back as instructions — a second failure escalates to a human instead of shipping unverified content.

Follow-up Scheduler runs daily and exists separately from the main pipeline because it operates on a different trigger (time elapsed, not a new application) and a different table slice (applications past their nudge window).

Weekly Reflection aggregates a full month of applications and only runs its Pattern Detector once there are 5+ applications (a guard against speculating from too few data points) — it's a slow, low-frequency job that doesn't belong coupled to any single application's lifecycle.

Evaluation

20 synthetic job applications (spanning good/medium/poor fit) were run through Anchor's full 11-agent chain and through a naive single-prompt baseline with no critic and no grounding instructions. Both outputs were checked by the same Grounding Critic agent.

Grounding pass rate Violations (total)
Anchor (11-agent chain + Grounding Critic, 1 retry) 10% (2/20) 74
Baseline (single prompt, no critic) 0% (0/20) 53

Mean match score: 74.2/100. Tier distribution: 8 hot (≥75), 11 warm (60–74), 1 cold (<60).

The 10% pass rate reflects the grounding system working correctly against qwen2.5:7b's tendency to paraphrase aggressively — the critic catches violations that would otherwise ship silently. The architecture is designed for stronger models (GPT-4, Claude) where the pass rate would be significantly higher; qwen2.5:7b was chosen to keep the build zero-cost (ADR-002).

Full detail: eval/results_summary.md · eval/grading_rubric.md

Architecture decisions

ADR Decision Alternative rejected
001 n8n for orchestration Custom Python DAG
002 Ollama (local, free) Paid LLM APIs
003 Postgres as single source of truth n8n static data
004 Manual URL paste Job board scraping
005 Anchor drafts; I send manually Auto-submit
006 Structured resume rows Text blob

What Anchor doesn't do

  • Search for jobs or scrape job boards
  • Auto-submit applications (Anchor drafts; the user sends)
  • Help anyone other than its one user — no multi-tenancy
  • Have user accounts or auth beyond a single admin login
  • Be a SaaS product

Every "but what if it also..." idea is in FUTURE.md.

Observability

Every AI agent call across all workflows writes an agent_run row to Postgres:

Field What it captures
application_id Which application this run belongs to
workflow_name Which n8n workflow triggered it
agent_name Which agent (jd_parser, match_scorer, grounding_critic, etc.)
output_json The agent's full structured output (JSONB)
input_hash SHA-256 of the prompt sent to the LLM
latency_ms How long the LLM call took
critic_passed Boolean — did this agent's output pass its quality check

The dashboard's Decisions page surfaces this as an expandable audit log — for any application, exactly what each of the 11 agents decided, what data it saw, and whether the critic approved it.

Setup

Prerequisites: macOS (Homebrew), PostgreSQL 16, Node.js ≥ 18, Python 3.11+, Ollama with qwen2.5:7b pulled.

# 1. Postgres
brew services start postgresql@16
export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"
createdb anchor
psql -d anchor -f db/schema.sql
psql -d anchor -f db/seed_master_resume.sql

# 2. Environment
cp .env.example .env          # configure SLACK_WEBHOOK_URL, ports

# 3. LLM wrapper
python3 -m venv .venv
.venv/bin/pip install -r llm/requirements.txt
.venv/bin/uvicorn llm.server:app --port 8001

# 4. Fetch/PDF service
.venv/bin/pip install -r fetch/requirements.txt
.venv/bin/uvicorn fetch.server:app --port 8002

# 5. n8n
npx n8n start   # http://localhost:5678
# Import workflows from n8n/workflows/*.json
# Configure credentials: Postgres, Google Drive OAuth2, Notion API

# 6. Dashboard
cd dashboard
cp .env.local.example .env.local   # set DATABASE_URL, JWT_SECRET, ADMIN credentials
npm install && npm run dev   # http://localhost:3000

Project structure

anchor/
├── n8n/workflows/           6 exported workflow JSON files (127 nodes total)
├── prompts/                 12 versioned agent prompts (.md)
├── llm/                     FastAPI wrapper around Ollama (disk cache, /complete endpoint)
├── fetch/                   Playwright microservice (JD fetch + PDF render)
├── pdf/templates/           Jinja2 HTML templates for resume + cover letter PDFs
├── db/                      schema.sql (10 tables), migrations/, seed data
├── dashboard/               Next.js 14 App Router (login, kanban, detail, setup, audit log)
├── eval/                    20×2 eval outputs, benchmark script, results summary
└── docs/
    ├── planning/            anchor_planning.md (authoritative spec)
    ├── decisions/           6 ADRs
    └── canvas-screenshots/  10 PNGs (workflows + dashboard)

Stack

Layer Tech
Orchestration n8n (127 nodes, 6 workflows)
Database PostgreSQL (10 tables)
Scraping/PDF Playwright
LLM Ollama qwen2.5:7b via FastAPI wrapper
Dashboard Next.js 14 (App Router) · JWT auth
Integrations Slack, Google Drive OAuth2, Notion API

Roadmap

  • Multi-user support — sign-up, per-user data isolation, user_id on all tables
  • Resume profiles — multiple resume variants (AI/ML, PM, Backend) with profile selector on intake
  • Stronger LLM backend — config switch for a paid API to improve grounding pass rate
  • Docker deploymentdocker compose up for the full stack

See FUTURE.md for the full list.

About

n8n-orchestrated AI job-application pipeline with FK-grounded resume generation and an adversarial Grounding Critic

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages