> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anyformat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Resume parsing

> Pull contact details, skills, education and work history out of PDF and DOCX resumes

**Nodes used:** [Parse](/guides/nodes/parse), [Extract](/guides/nodes/extract)

This example turns a resume into candidate data you can filter on: name, contact details, years of experience, and three tables of rows for skills, education and work history. Each table is an `object` field, which is how a repeating list is modelled on the typed graph.

## The workflow

<CodeGroup>
  ```json API theme={null}
  {
    "name": "Resume parsing",
    "description": "Extract candidate details from resumes",
    "nodes": [
      { "id": "parse_1", "type": "parse" },
      {
        "id": "extract_1",
        "type": "extract",
        "extraction_schema": {
          "fields": [
            { "name": "candidate_name",      "description": "Full name of the candidate", "data_type": "string" },
            { "name": "email",               "description": "Email address", "data_type": "string" },
            { "name": "phone",               "description": "Phone number including country code if present", "data_type": "string" },
            { "name": "years_of_experience", "description": "Total years of professional work experience", "data_type": "integer" },
            {
              "name": "skills",
              "description": "Technical skills, programming languages and tools, one row per skill",
              "data_type": "object",
              "nested_fields": [
                { "name": "skill", "description": "One skill, language or tool", "data_type": "string" }
              ]
            },
            {
              "name": "education",
              "description": "Educational qualifications and degrees, one row per degree",
              "data_type": "object",
              "nested_fields": [
                { "name": "institution",     "description": "University or school name", "data_type": "string" },
                { "name": "degree",          "description": "Degree obtained (e.g. BSc CS, MBA)", "data_type": "string" },
                { "name": "graduation_date", "description": "Date of graduation", "data_type": "date" }
              ]
            },
            {
              "name": "work_history",
              "description": "Previous jobs and roles, most recent first, one row per job",
              "data_type": "object",
              "nested_fields": [
                { "name": "company",    "description": "Company name", "data_type": "string" },
                { "name": "title",      "description": "Job title", "data_type": "string" },
                { "name": "start_date", "description": "Start date of employment", "data_type": "date" },
                { "name": "end_date",   "description": "End date of employment, or empty if this is the current role", "data_type": "date" }
              ]
            }
          ]
        }
      }
    ],
    "edges": [{ "source": "parse_1", "target": "extract_1" }]
  }
  ```

  ```python Python theme={null}
  import os
  from anyformat.sdk import Client
  from anyformat.workflow import Schema

  client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])

  workflow = (
      client.workflow("Resume parsing", "Extract candidate details from resumes")
      .parse()
      .extract([
          Schema.string("candidate_name",       "Full name of the candidate"),
          Schema.string("email",                "Email address"),
          Schema.string("phone",                "Phone number including country code if present"),
          Schema.integer("years_of_experience", "Total years of professional work experience"),
          Schema.object("skills", "Technical skills, programming languages and tools, one row per skill", fields=[
              Schema.string("skill", "One skill, language or tool"),
          ]),
          Schema.object("education", "Educational qualifications and degrees, one row per degree", fields=[
              Schema.string("institution",   "University or school name"),
              Schema.string("degree",        "Degree obtained (e.g. BSc CS, MBA)"),
              Schema.date("graduation_date", "Date of graduation"),
          ]),
          Schema.object("work_history", "Previous jobs and roles, most recent first, one row per job", fields=[
              Schema.string("company",  "Company name"),
              Schema.string("title",    "Job title"),
              Schema.date("start_date", "Start date of employment"),
              Schema.date("end_date",   "End date of employment, or empty if this is the current role"),
          ]),
      ])
      .create()
  )
  ```

  ```typescript TypeScript theme={null}
  import { Anyformat, Schema } from "@anyformat/sdk";

  const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });

  const workflow = await af
    .workflow("Resume parsing", "Extract candidate details from resumes")
    .parse()
    .extract([
      Schema.string("candidate_name",       "Full name of the candidate"),
      Schema.string("email",                "Email address"),
      Schema.string("phone",                "Phone number including country code if present"),
      Schema.integer("years_of_experience", "Total years of professional work experience"),
      Schema.object("skills", "Technical skills, programming languages and tools, one row per skill", [
        Schema.string("skill", "One skill, language or tool"),
      ]),
      Schema.object("education", "Educational qualifications and degrees, one row per degree", [
        Schema.string("institution",   "University or school name"),
        Schema.string("degree",        "Degree obtained (e.g. BSc CS, MBA)"),
        Schema.date("graduation_date", "Date of graduation"),
      ]),
      Schema.object("work_history", "Previous jobs and roles, most recent first, one row per job", [
        Schema.string("company",  "Company name"),
        Schema.string("title",    "Job title"),
        Schema.date("start_date", "Start date of employment"),
        Schema.date("end_date",   "End date of employment, or empty if this is the current role"),
      ]),
    ])
    .create();
  ```
</CodeGroup>

## Run it and read the result

The `files` field accepts PDF, DOCX and images. Poll the run until its `status` is `processed`.

<CodeGroup>
  ```bash curl theme={null}
  # 1. Create the workflow with the JSON above
  curl -X POST 'https://api.anyformat.ai/v3/workflows/' \
    -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
    -H 'Content-Type: application/json' \
    -d @resume-workflow.json
  # → 201 { "id": "<workflow_id>", ... }

  # 2. Upload the resume and start a run
  curl -X POST "https://api.anyformat.ai/v3/workflows/$WORKFLOW_ID/upload/run/" \
    -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
    -F 'files=@resume.docx'
  # → 202 { "run_id": "<run_id>", "document_packet_id": "...", "status": "queued" }

  # 3. Poll until "status" is "processed"
  curl "https://api.anyformat.ai/v3/runs/$RUN_ID/" \
    -H "Authorization: Bearer $ANYFORMAT_API_KEY"
  ```

  ```python Python theme={null}
  result = workflow.run("resume.docx").wait()

  print(result.fields["candidate_name"].value)
  print(result.fields["years_of_experience"].value)

  skills = [row["skill"].value for row in result.fields["skills"]]
  for job in result.fields["work_history"]:
      print(job["company"].value, job["title"].value, job["start_date"].value, job["end_date"].value)
  ```

  ```typescript TypeScript theme={null}
  import { readFileSync } from "node:fs";
  import type { ExtractedField, ExtractedRows } from "@anyformat/sdk";

  const file = new File([readFileSync("resume.docx")], "resume.docx");
  const result = await (await workflow.run(file)).wait();

  console.log((result.field("candidate_name") as ExtractedField<string> | undefined)?.value);
  console.log((result.field("years_of_experience") as ExtractedField<string> | undefined)?.value);

  const skills = (result.field("skills") as ExtractedRows).map((row) => row.skill?.value);
  for (const job of result.field("work_history") as ExtractedRows) {
    console.log(job.company?.value, job.title?.value, job.start_date?.value, job.end_date?.value);
  }
  ```
</CodeGroup>

## Response

The `results` envelope of a processed run, trimmed. A current role comes back with `end_date.value: null`.

```json theme={null}
{
  "status": "processed",
  "results": {
    "extractions": [
      {
        "split_name": null,
        "partition": null,
        "fields": {
          "candidate_name": {
            "value": "Maria Gonzalez",
            "confidence": 98.0,
            "evidence": [{ "text": "MARIA GONZALEZ", "page_number": 1 }],
            "verification_status": "not_verified",
            "value_override": null
          },
          "years_of_experience": {
            "value": "9",
            "confidence": 95.0,
            "evidence": [{ "text": "9 years of professional experience", "page_number": 1 }],
            "verification_status": "not_verified",
            "value_override": null
          },
          "skills": [
            { "skill": { "value": "Python", "confidence": 98.0, "evidence": [], "verification_status": "not_verified", "value_override": null } },
            { "skill": { "value": "Go",     "confidence": 98.0, "evidence": [], "verification_status": "not_verified", "value_override": null } }
          ],
          "work_history": [
            {
              "company":    { "value": "Fintonic",               "confidence": 97.0, "evidence": [], "verification_status": "not_verified", "value_override": null },
              "title":      { "value": "Staff Backend Engineer", "confidence": 97.0, "evidence": [], "verification_status": "not_verified", "value_override": null },
              "start_date": { "value": "2021-03-01",             "confidence": 90.0, "evidence": [], "verification_status": "not_verified", "value_override": null },
              "end_date":   { "value": null,                     "confidence": 90.0, "evidence": [], "verification_status": "not_verified", "value_override": null }
            }
          ]
        }
      }
    ]
  }
}
```

## Tips

* DOCX resumes give better results than scanned PDFs. The text is native, so nothing is lost to OCR.
* There is no `list` data type. Model a free-form list as an `object` with one nested field, as `skills` does here. See [Field types](/concepts/field-types).
* Describe `end_date` as "empty if this is the current role". The model then returns `null` instead of guessing a date.
* Declare `years_of_experience` as `integer`. You can filter candidates on it without parsing text.
* Resumes state dates as "March 2021". A `date` field normalises them to the first of the month.

## Next steps

<CardGroup cols={2}>
  <Card title="Upload and run" icon="play" href="/api-reference-v3/workflows/upload-and-run">
    The multipart request, idempotency and filename rules
  </Card>

  <Card title="Field types" icon="diagram-project" href="/concepts/field-types">
    Objects, enums and the other field shapes
  </Card>
</CardGroup>
