PDF extraction
Use okraPDF with Claude Cowork and Claude Skills
A tested workflow for giving Claude Code or Cowork a PDF extraction skill backed by okraPDF's files, parse, and jobs API.
Claude Code, Claude Cowork, and Claude Skills all point at the same practical pattern: keep the repeatable workflow in a small bundle, keep the sensitive API key in the environment, and let Claude call a deterministic script when it needs work done.
For PDFs, that script should not try to make the model read raw PDF bytes. It should hand the document to a PDF API, wait for a structured result, then return compact text or JSON that Claude can use.
I tested that flow with okraPDF before writing this. The smoke test uploaded a one-page timesheet PDF through POST /v1/files, parsed page 1 with POST /v1/parse, then polled GET /v1/jobs/{job_id} until the job reached succeeded. The returned result included page text for the employee, week ending date, time entries, total hours, and total pay.
That is the whole reason this works well as a Claude Skill: the skill does not need to teach Claude how to parse a PDF. It only needs to teach Claude when and how to call okraPDF.
Why this fits Claude Skills
Claude Skills are directories with a SKILL.md file plus optional scripts and resources. Anthropic’s docs describe them as dynamically loaded instructions for specialized tasks, and the custom-skill guide explicitly supports executable scripts in a scripts/ directory.
Claude Code skills use the same idea in the coding workflow. A project or user skill can tell Claude how to handle a repeatable task, and Claude can invoke it when the task description matches.
Claude Cowork is useful when the output is a workplace deliverable instead of a code change: summarize a folder of vendor invoices, turn PDFs into a spreadsheet, prepare a report from source documents, or organize files after extraction. Cowork can bundle skills, connectors, and sub-agents into a specialist workflow.
okraPDF is the tool behind the skill. Claude decides what needs to be extracted. okraPDF does the PDF work.
The skill shape
A minimal okraPDF skill can be this small:
okra-pdf-extract/
SKILL.md
scripts/
parse-pdf.mjs
The SKILL.md should be short and operational:
---
name: okra-pdf-extract
description: Extract text and structured data from PDF files using the okraPDF API.
---
Use this skill when the user asks you to read, summarize, extract, tabulate, or validate data from a PDF.
Rules:
- Use `scripts/parse-pdf.mjs` for local PDF files.
- Require `OKRA_API_KEY` in the environment.
- Do not paste raw API keys into commands or output.
- Prefer page-scoped extraction when the user only needs a specific page range.
- Return compact evidence: file id, job id, job status, page numbers, and the extracted text or fields needed for the task.
The script does the actual work. This version uses only platform APIs available in Node 22:
// scripts/parse-pdf.mjs
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
const [pdfPath, pages = "1"] = process.argv.slice(2);
if (!pdfPath) {
throw new Error("Usage: node scripts/parse-pdf.mjs ./file.pdf [pages]");
}
const apiKey = process.env.OKRA_API_KEY;
if (!apiKey) {
throw new Error("Set OKRA_API_KEY before running this script.");
}
async function okra(path, init = {}) {
const res = await fetch(`https://api.okrapdf.com${path}`, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
...(init.headers || {}),
},
});
const text = await res.text();
if (!res.ok) {
throw new Error(`${init.method || "GET"} ${path} failed: ${res.status} ${text}`);
}
return text ? JSON.parse(text) : {};
}
const bytes = await readFile(pdfPath);
const form = new FormData();
form.append("file", new Blob([bytes], { type: "application/pdf" }), basename(pdfPath));
const uploaded = await okra("/v1/files", {
method: "POST",
body: form,
});
const fileId = uploaded.id || uploaded.file_id;
const job = await okra("/v1/parse", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file: { id: fileId },
file_name: basename(pdfPath),
parser: "textlayer",
pages,
outputs: { markdown: true, json: true },
}),
});
let current = job;
for (let i = 0; i < 20; i += 1) {
if (["succeeded", "failed", "cancelled", "completed", "completed_with_errors"].includes(current.status)) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 1500));
current = await okra(`/v1/jobs/${job.id}`);
}
const pagesOut = current.result?.output?.pages || [];
const text = pagesOut
.flatMap((page) => page.blocks || [])
.map((block) => block.value || block.text || "")
.filter(Boolean)
.join("\n\n");
console.log(JSON.stringify({
file_id: fileId,
job_id: job.id,
status: current.status,
pages: pagesOut.map((page) => page.pageNumber),
text,
}, null, 2));
Run it directly before handing it to Claude:
OKRA_API_KEY=okra_... node scripts/parse-pdf.mjs ./timesheet.pdf 1
The test I ran followed this exact sequence. The final job status was succeeded, and the text output contained the timesheet summary:
Total Hours 38.0
Regular Hours 40.0
Overtime Hours 0.0
Hourly Rate $35.00
Total Pay $1,330.00
Status: Approved
Using it in Claude Code
For Claude Code, put the folder at either:
.claude/skills/okra-pdf-extract/
for a project skill, or:
~/.claude/skills/okra-pdf-extract/
for a personal skill.
Then ask for the outcome, not the plumbing:
Use the okraPDF skill to read ./vendor-invoice.pdf and return invoice number, vendor name, due date, total, and line items as JSON.
Claude should load the skill, run the script, inspect the returned JSON, and answer from the extracted text. If it needs a different page range, it can pass a second argument:
node scripts/parse-pdf.mjs ./contract.pdf 2-5
That keeps the agent loop simple:
- Claude decides what evidence it needs.
- The script uploads and parses the PDF.
- okraPDF returns a job result.
- Claude summarizes, validates, or reshapes the result.
No PDF bytes go into the model context. No one asks Claude to OCR a document by guessing from screenshots. The agent gets text and structure it can reason over.
Using it in Cowork
Cowork is a better fit when the source PDFs live in a working folder and the desired output is a business artifact.
Good prompts look like this:
Use the okraPDF extraction skill on every PDF in this folder. Create a spreadsheet with vendor, invoice number, invoice date, due date, subtotal, tax, total, and a notes column for uncertain fields.
or:
Read the three policy PDFs in this folder. Produce a comparison memo with page-cited differences in eligibility, exclusions, and renewal terms.
The important constraint is that the skill should return evidence, not a finished opinion. Let okraPDF extract text, page blocks, tables, or fields. Let Claude assemble the spreadsheet, memo, or checklist from that evidence.
When MCP is the better interface
A local Skill is the fastest thing to ship when the workflow is personal or team-specific. It is just files, scripts, and instructions.
For a shared organization-wide integration, use MCP. Claude Code can connect to remote tools through Model Context Protocol, which is a better fit when you want centrally managed auth, shared tool descriptions, and a stable tool surface across many users.
The split is straightforward:
| Need | Use |
|---|---|
| Personal workflow over local PDFs | Claude Skill |
| Team workflow packaged with instructions and scripts | Claude Skill or Cowork plugin |
| Shared remote API tools with managed auth | MCP |
| High-volume production ingestion | okraPDF API directly |
You can start with the Skill above, then promote the stable parts into MCP once the workflow proves itself.
API notes from the test
The tested sequence used three okraPDF endpoints:
POST /v1/files
POST /v1/parse
GET /v1/jobs/{job_id}
The upload response returned a reusable file_id. The parse request accepted that file_id, a parser choice, a page range, and output preferences. The job response exposed status, progress, and result.output.pages.
That makes it agent-friendly in a boring way:
- Upload is separate from parsing, so Claude can reuse the same file.
- Parsing is asynchronous, so longer documents do not block a single request.
- Job status is machine-readable, so scripts can poll safely.
- Page output is compact enough to hand back to Claude without dumping an entire PDF.
For a first skill, use textlayer on digitally generated PDFs. For scanned PDFs, handwritten forms, or image-heavy documents, route to the OCR/parser configured for your okraPDF account.
The takeaway
Claude Skills are not a replacement for a PDF parser. They are a clean place to package the habit of using one.
okraPDF gives the skill a small reliable API surface: upload a PDF, start a parse job, poll the job, return page-grounded output. Claude Code can use that inside a repo. Cowork can use it across workplace folders. MCP can turn the same pattern into a managed shared tool.
Start with the local skill because it is easy to inspect. Once the workflow is boring and useful, turn it into a connector.