The problem with auto-applying to jobs is that a generic resume gets ignored. The insight: different roles need different emphasis. A startup wants to see velocity and ownership. An enterprise team wants to see process and scale. Same resume, different angles.
I built a job archetype classifier that runs before scoring. It detects what type of engineer the company actually wants, then tells the scoring system which parts of your background to emphasize.
The Seven Archetypes
Not all "Senior Engineer" roles are the same. I defined seven hiring intents:
type JobArchetype =
| 'STARTUP_BUILDER' // Move fast, ship features, wear hats
| 'ENTERPRISE_ENGINEER' // Scale, reliability, process
| 'SAAS_PLATFORM_ENGINEER' // Multi-tenant, API design, growth
| 'FINTECH_TRANSACTION_ENGINEER' // Compliance, accuracy, security
| 'FRONTEND_PRODUCT_ENGINEER' // UX craft, A/B tests, metrics
| 'BACKEND_INFRA_ENGINEER' // Uptime, throughput, observability
| 'FULLSTACK_GENERALIST' // Breadth over depth
Each archetype maps to what the company is really hiring to mitigate. A fintech doesn't want your "move fast and break things" story. A seed-stage startup doesn't care about your SOC2 implementation.
The Classification Flow
Before scoring, every job goes through classification:
interface ArchetypeResult {
primaryArchetype: JobArchetype;
secondaryArchetype: JobArchetype | null;
confidence: number;
resumeWeighting: ResumeWeighting;
}
interface ResumeWeighting {
emphasize: string[]; // "distributed systems", "team leadership"
deEmphasize: string[]; // "rapid prototyping", "hackathons"
toneBias: string; // "reliability_and_correctness"
}
The LLM reads the job description and returns both the archetype AND specific instructions for how to angle your resume. These weightings get stored on the job:
@Column({ type: 'varchar', length: 50, nullable: true })
archetype: JobArchetype | null;
@Column({ type: 'jsonb', nullable: true })
resumeWeighting: ResumeWeighting | null;
Why Two Models?
I'm using different LLMs for classification vs scoring:
// Main LLM for scoring (needs reasoning depth)
this.client = new OpenAI({
apiKey: this.configService.get('llm.apiKey'),
baseURL: this.configService.get('llm.baseUrl'),
});
// Cheaper classifier (pattern matching, less reasoning)
this.classifierClient = new OpenAI({
apiKey: this.configService.get('llm.classifier.apiKey'),
baseURL: this.configService.get('llm.classifier.baseUrl'),
});
Classification is cheaper and faster than scoring. I can run it on every job without blowing the budget. The classifier just needs to recognize patterns in job descriptions. The scorer needs to reason about fit.
The System Prompt Strategy
The classifier uses a scoring system, not vibes:
1. Read the job posting thoroughly.
2. Identify hiring intent signals: keywords, phrases, company context.
3. Score each archetype:
- Trigger keyword match = +2 points
- Context signal match = +3 points
- Negative signal = -2 points
4. Select primary archetype (highest score)
5. Select secondary if within 30% of primary
Each archetype has specific signals. For STARTUP_BUILDER:
Triggers: "founding", "0-to-1", "early stage", "move fast"
Contexts: Mentions small team size, equity compensation, ambiguity
Negatives: "Fortune 500", "established processes", "legacy"
The model counts signal matches and outputs a confidence score. Low confidence (< 50) means the posting is too generic or vague.
What Actually Changed in the Code
The big addition is the ResearchModule that handles pre-scoring analysis. When a job moves from QUEUED to RESEARCHING:
async classifyArchetype(
jobText: string,
companyContext?: string
): Promise<ArchetypeResult> {
const response = await this.classifierClient.chat.completions.create({
model: this.classifierModel,
messages: [
{ role: 'system', content: ARCHETYPE_SYSTEM_PROMPT },
{ role: 'user', content: this.buildClassificationPrompt(jobText, companyContext) }
],
tools: [CLASSIFY_ARCHETYPE_TOOL],
tool_choice: { type: 'function', function: { name: 'save_archetype_classification' }}
});
// Parse tool call response...
}
The job entity stores the classification result:
// In Job entity
markResearching(): void {
if (this.status !== ApplicationStatus.QUEUED) {
throw new InvalidJobStatusError(/*...*/);
}
this.status = ApplicationStatus.RESEARCHING;
}
markResearchComplete(archetype: JobArchetype, weighting: ResumeWeighting): void {
this.archetype = archetype;
this.resumeWeighting = weighting;
this.status = ApplicationStatus.APPLYING;
}
The POC That Validated This
I wrote archetype-eval.poc.ts to test three models on classification accuracy:
const MODELS = [
{ name: 'GLM-4.7-Flash', inputCost: 0.07, outputCost: 0.40 },
{ name: 'GLM-5', inputCost: 1.0, outputCost: 3.2 },
{ name: 'Kimi K2.5', inputCost: 0.6, outputCost: 3.0 }
];
GLM-4.7-Flash won: 94% accuracy at 7x lower cost. The classification task doesn't need the reasoning depth of the larger models.
Why This Matters
Generic applications get filtered out. Humans scan for relevance in 10 seconds. The archetype classification gives the system the context it needs to highlight the right 10 seconds of your background.
A job at a payments company gets classified as FINTECH_TRANSACTION_ENGINEER. The scoring system then knows to emphasize your "implemented idempotent payment processing" experience over your "shipped features fast" startup work.
Same resume. Different emphasis. Better match rate.
The code's live at github.com/Anietex/huntly.
Top comments (0)