docs: OpenJob and OpenResume.md (#148)
Some checks failed
CI / build (push) Has been cancelled
test / test (push) Has been cancelled

Two conventions for the hiring end of the agentic stack, published here beside
the other Open* specs.

OpenResume.md says a resume is a Markdown file: a document a person can read,
diff and keep, and one an agent can write without being taught a schema first.
Six conventions, every one of which degrades rather than fails, because a
resume that does not parse still has to be a usable resume.

OpenJob extends schema.org JobPosting with the three things it has no
vocabulary for: whether the employer accepts applications written with an agent
(stated, rather than discovered by silent rejection), the application form as
data so applying does not require rendering a page, and a description in
Markdown.

Both are implemented by profullstack/agenticjobs, and neither requires it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-09-08 16:16:05 -07:00 committed by GitHub
parent 88b29da91c
commit 331b437724
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 345 additions and 0 deletions

View file

@ -15,6 +15,8 @@ export const DOC_SLUGS = [
"openontology",
"openontology-governance",
"openontology-interoperability",
"openjob",
"openresume",
"openspec-comparison",
"data-model",
"cli",

200
docs/openjob.md Normal file
View file

@ -0,0 +1,200 @@
# OpenJob
A job posting that an agent can read, and that says out loud whether an agent may
answer it.
Job postings already have a machine-readable format: schema.org `JobPosting`, which
search engines index and which most boards emit. OpenJob does not replace it. It
extends it with the three things `JobPosting` has no vocabulary for, and which the
next few years of hiring depend on:
1. **whether the employer accepts applications written with an agent**, stated
rather than discovered by silent rejection;
2. **the application form, as data**, so applying does not require rendering a page;
3. **a description in Markdown**, so the same text is legible to a person, a model
and a terminal without a HTML-to-text round trip.
## The document
A listing is a JSON object. Every field below is what the reference implementation
serves at `GET /api/v1/jobs/{slug}`.
```json
{
"id": "b7c3...",
"slug": "staff-engineer-agent-platform",
"title": "Staff Engineer, Agent Platform",
"description": "We are building the plumbing...",
"org": {
"slug": "example-works",
"name": "Example Works",
"website": "https://example.com"
},
"employmentType": "full-time",
"workplace": "remote",
"seniority": "staff",
"location": "European timezones",
"remoteRegions": ["DE", "NL", "PT"],
"salary": {
"min": 180000,
"max": 230000,
"currency": "USD",
"period": "year",
"equity": "0.1% - 0.4%"
},
"tags": ["infrastructure", "agents"],
"stack": ["typescript", "postgres", "rust"],
"requirements": ["Has run something other people depended on."],
"responsibilities": ["Own the execution layer."],
"agentPolicy": "welcome",
"apply": { "via": "board", "schema": { "fields": [] } },
"status": "published",
"publishedAt": "2026-09-08T10:00:00.000Z",
"expiresAt": null
}
```
`description` is Markdown. Not HTML, and not plain text with the formatting removed.
## agentPolicy
The field this format exists for. Required on every listing, with exactly three
values:
| Value | Means |
| --- | --- |
| `welcome` | Agent-written applications are fine. Nothing is asked. |
| `disclose` | Fine, but say so. The application carries a structured disclosure. |
| `human-only` | The employer is asking for something a person wrote. |
Three things about it are deliberate.
**It is required.** An optional field would be omitted by most posters, and "not
stated" is exactly the ambiguity candidates are navigating today by guessing.
**`human-only` is a request, not a control.** No board can tell who wrote a cover
letter, and one that claims it can is selling something. Stating it plainly is worth
more than pretending to enforce it: a candidate who reads `human-only` and writes it
themselves has been told what the employer wants, which is all anyone can offer.
**Disclosure is not evidence against the candidate.** A board that collects the
disclosure and then filters those applications out has broken the field for
everybody, because the next candidate learns to lie. `disclose` means the employer
wants to know, and wanting to know is the reason to answer honestly.
## apply
Three shapes, and only the first is completable without a browser.
```json
{ "via": "board", "schema": { "fields": [ ... ] } }
{ "via": "url", "url": "https://example.com/careers/123" }
{ "via": "email", "email": "jobs@example.com" }
```
A listing that says `url` or `email` is being honest that an agent cannot finish the
job. That is better than a board pretending every listing is applicable and handing
an agent a form it cannot post.
### The application schema
A deliberately small subset of JSON Schema: small enough to render as an HTML form,
small enough for a model to fill in without a validator.
```json
{
"fields": [
{ "name": "name", "label": "Your name", "type": "text", "required": true, "maxLength": 120 },
{ "name": "email", "label": "Email", "type": "email", "required": true, "maxLength": 200 },
{ "name": "cover", "label": "Why you", "type": "textarea", "required": true, "maxLength": 5000,
"help": "Plain text. Short is fine." }
]
}
```
`type` is one of `text`, `textarea`, `email`, `url`, `select` or `file`. A `select`
carries `options`.
An implementation publishes the schema at a stable address alongside the endpoint
that accepts it, so reading and answering are two requests and no guessing:
```
GET /api/v1/jobs/{slug}/apply-schema
POST /api/v1/jobs/{slug}/apply
```
### The disclosure
Posted alongside the answers:
```json
{
"agent": { "name": "claude-opus-5 via agenticjobs-mcp", "supervised": true }
}
```
`supervised` means a person read it before it was sent. It is never inferred - a
guessed disclosure is worthless in both directions.
### Resumes
A resume travels as Markdown, in the [OpenResume.md](/docs/openresume) convention, in
a `resume` field. Not a file upload, not a URL to a PDF. An employer receives text
they can read and an agent receives text it can write.
## Mapping onto schema.org
An OpenJob listing maps cleanly onto `JobPosting`, and an implementation should emit
both - the JSON-LD for search engines, the OpenJob document for everything else.
| OpenJob | JobPosting |
| --- | --- |
| `title` | `title` |
| `description` | `description` |
| `org` | `hiringOrganization` |
| `employmentType` | `employmentType` (`FULL_TIME`, `PART_TIME`, `CONTRACTOR`, `INTERN`, `TEMPORARY`) |
| `workplace: "remote"` | `jobLocationType: "TELECOMMUTE"` |
| `remoteRegions` | `applicantLocationRequirements` |
| `location` | `jobLocation` |
| `salary` | `baseSalary`, plus an annualised `estimatedSalary` |
| `expiresAt` | `validThrough` |
| `apply.via === "board"` | `directApply: true` |
The three fields with no equivalent - `agentPolicy`, `applyVia` and the address of
the application schema - travel in `additionalProperty`, which is the vocabulary's
own escape hatch and passes every validator.
One trap worth naming, because it catches almost everyone: a remote role needs
`jobLocationType: "TELECOMMUTE"` **and** a location the hire may sit in. A remote
posting with no `jobLocation` and no `applicantLocationRequirements` fails Google's
validation while looking entirely correct.
## Drafts
A listing has a `status`, and `draft` is the interesting one. A draft is not
published, not in the feed, not in the API's search results, and not visible to any
other instance.
This is the employer's human control point. An agent can write the listing; a person
publishes it. The reference implementation makes a job created over the API or by a
tool call a draft *unless the caller explicitly asks otherwise*, which is the correct
default the first time an agent posts a job its author has not read.
The candidate's side of that seam is an application that can be prepared and held
until a person sends it. Both ends of the transaction have one, or the design is
lopsided.
## Federation
An implementation that wants to be discoverable serves a descriptor at
`/.well-known/agenticjobs` naming its search endpoint, its OpenAPI document, its MCP
endpoint and its feed. A directory reads that descriptor from the instance itself
rather than trusting an announcement, and a client fans one query out across every
instance it knows.
Nothing about OpenJob requires federation, and nothing about it requires a directory.
A single self-hosted board that serves these documents is a complete implementation.
## Implementations
- `agenticjobs` - MIT, https://github.com/profullstack/agenticjobs

143
docs/openresume.md Normal file
View file

@ -0,0 +1,143 @@
# OpenResume.md
A resume is a Markdown file. That is the whole idea.
Not a form, not a PDF, not a proprietary JSON schema that one product understands.
A file a person can read, diff, keep in a repository, paste into any tool, and hand
to an employer without losing anything on the way. And a file an agent can write and
revise without being taught a schema first.
This document describes the convention. It is deliberately thin, because a resume
that fails to parse still has to be a usable resume.
## The shape
```markdown
# Ada Lovelace
- **Email**: ada@example.com
- **Location**: London
- **Web**: https://example.com
Mathematician, looking for work on machines that do not exist yet.
## Experience
### Analytical Engine | London
Chief Programmer (1842 - 1843)
- Wrote the first published algorithm intended to be carried out by a machine.
- Described what the engine could do beyond arithmetic, which its designer had not.
## Skills
- Languages: analytical notation, French
- Tools: difference engine, correspondence
## Education
### University of London
Private tuition in mathematics (1840)
```
## The rules
There are six, and every one of them degrades rather than fails.
**1. One `#` heading, and it is the person's name.** A document with more than one
is read using the first; a document with none still parses, and a reader that wants a
name can say it does not have one.
**2. The bullet list directly under the name is the contact block.** Each item is
`Key: value`, with or without `**bold**` on the key, or a bare `[label](url)`. Values
that look like an email address, a phone number or a URL become links; anything else
stays text. Unknown keys are kept as written - `Pronouns`, `Timezone` and
`Availability` all work without anyone having to add them to a list.
**3. A single prose line between the contact block and the first `##` is a
headline.** One line. More than one, and only the first is treated that way.
**4. `##` opens a section.** The text is kept verbatim, and separately normalised for
matching, so `Work Experience`, `Experience` and `Employment` are one thing to a
filter and three different words on the page. The normalised names in common use are
`experience`, `education`, `skills`, `projects`, `summary`, `links`,
`certifications`, `languages`, `publications` and `awards`. A section whose name
matches none of them keeps its own name and is not dropped.
**5. `###` opens an entry within a section: a job, a degree, a project.** A `|` in
the heading splits it into a title and a place - `Company | Berlin`. The first
non-empty line under the heading is the entry's subtitle: a role, a degree, a
one-line description. If that line ends in a bracketed range, the range is read out
of it:
```
Chief Programmer (1842 - 1843)
Staff Engineer (Mar 2020 - Present)
Contractor (2019 to 2021)
```
Hyphens, en dashes and em dashes are all accepted, because all three appear in real
resumes. `Present`, `Now`, `Current` and `Ongoing` mark a current role. Dates are
kept as the strings they were written as, and never reformatted: `Mar 2020` and
`03/2020` both survive, and a reader that wants a date can parse one.
**6. Bullets under an entry are its highlights.** Everything else under the entry is
kept verbatim, so nothing a person wrote is ever silently dropped.
## What is deliberately absent
**No required fields.** A document consisting of a name and three paragraphs is a
valid OpenResume.md.
**No date format.** Every attempt to impose one on resumes has failed, because people
write `Summer 2019` and mean it.
**No schema version.** Readers ignore what they do not recognise. A resume written
today has to be readable in five years by software nobody has written yet, which
means the format cannot have a version negotiation in it.
**No structured skills taxonomy.** Skills are the lines people wrote. Mapping them
onto a controlled vocabulary is a job for whatever is reading, and doing it at write
time destroys the information.
## Sections a parser derives, not the author
A reader may compute a structured view - name, contact pairs, sections, entries with
parsed date ranges - and use it for search and filtering. That view is derived, and
it is regenerated from the Markdown on every save.
**The Markdown is the canonical copy.** If the structured view and the document
disagree, the document is right. A product that stores the parse and treats the
Markdown as an export has not implemented this convention; it has implemented a form
with a Markdown skin, and the candidate no longer owns their resume.
## Converting into it
PDF, DOCX and plain text can all be converted to Markdown, and none of the
conversions are perfect. The rule that makes this workable: a converted document is
shown to the candidate to edit before it is used. A conversion nobody checks is a
conversion nobody should trust.
The `agenticjobs` reference implementation converts `.docx` by reading
`word/document.xml` directly, and `.pdf` with `pdftotext -layout`. Both are lossy in
ways a person spots in five seconds and a parser never will.
## Why Markdown
Because the alternative formats each fail one of the audiences.
A PDF is readable by a person and hostile to everything else; extracting a two-column
CV back into text loses the reading order, which is why so much hiring software
mangles them.
A JSON schema is readable by software and unwritable by a person; nobody drafts a
resume in JSON, so a tool has to generate it, and now the tool owns the resume.
Markdown is the only format that a person writes directly, a person reads directly,
a model writes well without being taught, `git diff` shows usefully, and a renderer
turns into a PDF when a PDF is genuinely what somebody wants.
## Implementations
- `agenticjobs` - the job board this convention was written for. MIT,
https://github.com/profullstack/agenticjobs