Skills vs. Prompts

Skills vs. Prompts: They're Not the Same Thing, and Confusing Them Will Bite You

I spent some time on an ATS integration project last year completely convinced I understood what I was building. The system used Claude to screen resumes, extract candidate data, and route applicants through a hiring pipeline. I kept treating the whole thing like a fancy prompt-and-response loop. Send the resume text, get structured output back, move on. Simple enough, right? Except the mental model was wrong, and it kept producing subtle failures that took forever to trace back to anything meaningful. Claude would extract skills correctly but forget the routing rules or it would classify a candidate as "qualified" but format the output differently each time, breaking the downstream pipeline. Or it would handle the first stage of screening fine and then behave completely differently in the follow-up stage, as if it had never seen the earlier instructions. The problem wasn't Claude. It was that I didn't really understand what a skill is, as distinct from a prompt , and I was using prompts to do skill-shaped work. What a Prompt Actually Is A prompt is stateless. That's the core thing. When you send a prompt to Claude via Spring AI, you're firing off a request. Claude reads the text, generates a response, and that's the transaction done. It doesn't remember you. The next call is a clean slate unless you explicitly include prior context, and that "memory" is entirely your problem to manage. Spring AI's ChatClient makes this feel almost too easy: import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; @Service public class ResumeExtractionService { private final ChatClient chatClient; public ResumeExtractionService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String extractCandidateData(String resumeText) { return chatClient.prompt() .system("Extract candidate name, skills, and years of experience from the resume. Return JSON.") .user(resumeText) .call() .content(); } } That's it. One shot. Claude reads the resume, returns JSON, done. The problem is what happens next. The ATS pipeline has multiple stages: extraction, qualification scoring, role matching, interview scheduling eligibility. Each stage has its own rules. Each stage needs to know what happened in the previous one. And each stage needs to behave consistently, not just "roughly the same way most of the time." If you model each stage as a standalone prompt, you get inconsistency baked in from the start. Claude improvises slightly differently on each call. The system prompt you wrote for stage one doesn't travel to stage two. You end up writing longer and longer system prompts trying to compensate, and it still doesn't hold. That's exactly what I did. And that's the trap. What Claude Skills Actually Are Claude Code has a skill system built around .claude folders, and once I understood it properly, it reframed the whole ATS problem. You place a .claude folder at the root of your project and store Markdown files inside it that define skills. These aren't prompts. They're named, reusable behavioral specifications that Claude loads as part of its context when it operates in your project. The structure looks like this: ats-backend/ ├── .claude/ │ ├── skills/ │ │ ├── resume-extraction.md │ │ ├── qualification-scoring.md │ │ ├── role-matching.md │ │ └── pipeline-routing.md │ └── settings.json ├── src/ └── pom.xml A skill file is Markdown, but what's inside it is what matters. Here's what qualification-scoring.md looks like in the ATS project: # Skill: Qualification Scoring ## When to apply When the user or system asks to score a candidate's qualification for a role, assess fit, or produce a hiring recommendation. ## Steps 1. Read the extracted candidate profile. Do not re-extract from raw resume text. 2. Compare candidate skills against the required skills list for the role. Score each required skill as: present, partial, or missing. 3. Calculate an overall fit score from 0 to 100 based on this weighting: - Required skills: 60% - Years of experience vs. requirement: 25% - Education match: 15% 4. Classify the candidate as: Strong Fit, Possible Fit, or Not a Fit. Use these thresholds: Strong >= 75, Possible >= 50, Not a Fit < 50. 5. List the top three reasons for the classification. Be specific, not generic. 6. If any required skill is missing entirely, flag it explicitly before giving the score. ## Constraints - Never infer skills that are not stated in the candidate profile. - Never adjust the score based on candidate name, location, or education institution prestige. - If the role requirements are ambiguous, output a warning and ask for clarification before scoring. ## Output format Return a JSON object with fields: fitScore, classification, reasons (array), missingRequiredSkills (array), warnings (array). No prose outside the JSON. That is not a prompt. It has a defined trigger condition, a numbered sequence of steps, explicit weighting rules, classification thresholds, hard constraints, and a specified output schema. Claude loads it, knows when to apply it, and follows it the same way every time, across every developer on the team, across every run of the pipeline. Why This Is Different from a System Prompt I tried replicating this with system prompts before I understood the skill system. It technically works for a single stage, but it falls apart when you have four stages that each need their own behavioral rules. You either write one enormous system prompt that tries to cover everything (and Claude loses track of which rules apply where), or you write four separate system prompts and paste them into four different service classes, which means they immediately drift out of sync when requirements change. Skills in .claude folders are modular. Each skill lives in its own file, in version control, alongside the code it governs. Your team reviews changes to qualification-scoring.md in the same pull request as changes to the scoring service. The behavioral specification and the implementation travel together. That's not possible with system prompts buried inside Java string literals. Skills also compose. The pipeline-routing.md skill can reference the classification output defined in qualification-scoring.md . Claude understands the relationship. A monolithic system prompt can't do that without becoming a wall of text that nobody wants to touch. How Skills Load and What Claude Does with Them When Claude Code starts in a project directory, it reads the .claude folder. The settings.json controls which skills are active and how Claude should handle them: { "skills": { "auto_load": true, "directory": ".claude/skills" }, "model": "claude-opus-4-5", "context": { "include_gitignore": false, "max_file_size_kb": 500 } } With auto_load set to true, Claude reads all the skill files at startup and treats them as standing instructions. When you ask it to score a candidate, it finds qualification-scoring.md , follows those steps, and applies those constraints. It doesn't improvise. It doesn't blend rules from different stages. It applies the skill that matches the task. This is the key difference. A prompt is you telling Claude what to do right now, in this call. A skill is a standing agreement about how Claude behaves in a category of situations, defined once, stored in your repo, applied consistently. The ATS pipeline I was building needed the second thing. I was giving it the first. The Spring AI Side of the Picture The .claude skill system governs Claude Code's behavior when it's helping you write and maintain the project. But the pipeline itself runs on Spring AI, and the two work together in a way that's worth making explicit. Here's the qualification scoring service wired up properly. The skill file defines the behavioral contract. The Java code implements...