{}
StrictJSON
Extraction

Resume Skills Extractor

Screening CVs by hand means re-reading the same pages to answer three questions: what can this person actually do, how deep does each skill go, and how much experience backs it up? This prompt extracts a structured skill profile from a resume: the candidate name, every skill with years of experience and a beginner-to-expert level, the top programming languages in order of prominence, and total years of experience. It is built for recruiters and hiring pipelines that need comparable data across hundreds of CVs, not free-text summaries. The traps are familiar: skills listed in a job title are not the same as skills demonstrated in the work, "familiar with" is not "advanced", and a five-year career rarely means five years in every listed skill. Years of experience stay null when the CV does not state them — never estimate from graduation dates — and the level follows the evidence in the CV, not the candidate's own adjectives.

System Prompt

You are a precise resume skill extraction engine. Read the provided resume text and return ONLY a valid JSON object matching the schema below. Do not add explanations, markdown, or extra fields. Extract the candidate_name exactly as printed on the resume. skills contains one object per distinct skill evidenced in the resume, with the skill name, a level of beginner, intermediate, advanced or expert, and years_experience. Set years_experience to null when the resume does not state a number of years for that skill; never estimate years from graduation dates or total career length. Judge the level from the evidence in the resume, such as years of use, seniority of the roles, and depth of the work described, not from the candidate's own adjectives. top_languages lists the main programming languages evidenced in the resume, in order of prominence, as an empty array when none are evidenced. total_years_experience is the total professional experience stated on the resume, or null when it is not stated and cannot be derived from dated roles without guessing. Never invent a skill, a language, a year count, or a level that is not evidenced in the resume. Never use an empty string for a required text field. If no candidate name is printed on the resume, do not produce any output.

Raw JSON Schema

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "ResumeSkills",
  "type": "object",
  "additionalProperties": false,
  "required": ["candidate_name", "skills", "top_languages", "total_years_experience"],
  "properties": {
    "candidate_name": {
      "type": "string",
      "minLength": 1
    },
    "skills": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["skill", "years_experience", "level"],
        "properties": {
          "skill": { "type": "string", "minLength": 1 },
          "years_experience": { "type": ["number", "null"], "minimum": 0 },
          "level": {
            "type": "string",
            "enum": ["beginner", "intermediate", "advanced", "expert"]
          }
        }
      }
    },
    "top_languages": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 }
    },
    "total_years_experience": {
      "type": ["number", "null"],
      "minimum": 0
    }
  }
}

Zod (TypeScript)

import { z } from "zod";

const SkillSchema = z.object({
  skill: z.string().min(1),
  years_experience: z.number().nonnegative().nullable(),
  level: z.enum(["beginner", "intermediate", "advanced", "expert"]),
}).strict();

export const ResumeSkillsSchema = z.object({
  candidate_name: z.string().min(1),
  skills: z.array(SkillSchema).min(1),
  top_languages: z.array(z.string().min(1)),
  total_years_experience: z.number().nonnegative().nullable(),
}).strict();

Pydantic v2 (Python)

from typing import Annotated, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator


class Skill(BaseModel):
    model_config = ConfigDict(extra="forbid")

    skill: str = Field(..., min_length=1)
    years_experience: float | None = Field(..., ge=0)
    level: Literal["beginner", "intermediate", "advanced", "expert"]

    @field_validator("skill", mode="before")
    @classmethod
    def _strip_skill(cls, v):
        if not isinstance(v, str):
            return v
        return v.strip()


class ResumeSkills(BaseModel):
    model_config = ConfigDict(extra="forbid")

    candidate_name: str = Field(..., min_length=1)
    skills: list[Skill] = Field(..., min_length=1)
    top_languages: list[Annotated[str, Field(min_length=1)]] = Field(...)
    total_years_experience: float | None = Field(..., ge=0)

    @field_validator("candidate_name", mode="before")
    @classmethod
    def _strip_name(cls, v):
        if not isinstance(v, str):
            return v
        return v.strip()

Try it in the StrictJSON tool →