object field, which is how a repeating list is modelled on the typed graph.
The workflow
{
"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" }]
}
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()
)
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();
Run it and read the result
Thefiles field accepts PDF, DOCX and images. Poll the run until its status is processed.
# 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"
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)
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);
}
Response
Theresults envelope of a processed run, trimmed. A current role comes back with end_date.value: 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
listdata type. Model a free-form list as anobjectwith one nested field, asskillsdoes here. See Field types. - Describe
end_dateas “empty if this is the current role”. The model then returnsnullinstead of guessing a date. - Declare
years_of_experienceasinteger. You can filter candidates on it without parsing text. - Resumes state dates as “March 2021”. A
datefield normalises them to the first of the month.
Next steps
Upload and run
The multipart request, idempotency and filename rules
Field types
Objects, enums and the other field shapes

