The career.json format
The canonical contract is
schemas/career.schema.json.
A fictional worked example is
examples/career.example.json.
Validate with make check.
Why it is shaped this way
Most of these fields exist because something went wrong without them.
Evidence atoms
One achievement, in STAR form.
{
"id": "E_PLATFORM_COST",
"title": "Cut build-pipeline cost by consolidating duplicate runners",
"star": { "situation": "...", "task": "...", "action": "...", "result": "..." },
"metrics": ["~33% CI spend reduction"],
"skills": ["platform engineering", "cost optimisation"],
"tags": ["cost-reduction"],
"occurred": { "start": "2023-04", "end": "2023-09", "inferred": false },
"employment_id": "EMP_CURRENT",
"evidence_status": "self_asserted",
"external_safe": true,
"outcome_type": "business_outcome",
"role_fit_notes": null,
"capture": { "method": "annual_review", "review_period": "FY2023" },
"source_refs": [{ "source_id": "SRC_RESUME", "locator": "page 1" }]
}
evidence_status — how well supported the claim is.
| Status | Means |
|---|---|
self_asserted |
Appears only in material you wrote. The normal resting state. |
corroborated |
A third party or public artefact supports it, no record captured |
externally_verified |
Backed by an independent source recorded in source_records |
unresolved |
An open question, recorded in open_questions |
declined |
You chose not to pursue it |
Your resume and your LinkedIn profile are both written by you. A claim in both is
self_asserted, not corroborated. Repetition is not corroboration — an earlier
version of this workspace marked eighteen atoms verified on exactly that
mistake.
Corroboration is optional. Its absence is not a defect and nothing chases it.
metrics — a metric is a claim, so it may carry how it was measured:
"metrics": [
{"value": "~33% CI spend reduction",
"basis": "monthly CI invoice, Q1 2023 against Q1 2024",
"measured": true},
"median build time unchanged"
]
A plain string is still valid and no pack needs migrating. The object form exists
because a figure whose baseline and denominator nobody recorded cannot be
defended: "67% throughput increase" is a strong bullet and an unanswerable
interview question. basis: null says the basis was not recorded, which is
honest; measured: false marks an estimate or a recollection rather than
something a system produced. select_evidence.py carries both through to
generation, so an unmeasured figure is visible before it reaches a page.
outcome_type — activity (work done: engagements, throughput), output
(things produced: patterns, courses), business_outcome (what changed for the
organisation). These describe what the evidence establishes; none is universally
preferred. Resume selection judges the contribution against the target role,
including useful outputs, prevention and work without financial measurements.
external_safe — false excludes a claim from external document generation.
Private career reviews and interview preparation may inspect withheld evidence;
that visibility does not grant permission to publish it. Wording acceptance and
external permission are separate choices in the human-review workflow.
occurred — when it happened. inferred: true means the dates came from the
employment window rather than being recorded, which for a nine-year tenure is
barely a date. Without this the pack cannot do recency, ordering, or gap
detection, which is most of what a career memory is for.
constraints — reviewed handling rules a generated claim must obey:
"prototype only", "team contribution, do not imply sole ownership". These reach
the selection view. notes is the operator's field and never does, which is why
internal names can live there safely and constraints cannot.
role_fit_notes — where the evidence counts against you. Media reach reads
as strength for an advocacy role and as "communicator, not builder" for a
hands-on engineering one.
Employment
Employers, titles, and dates. These carry no evidence ID of their own, so nothing else in the system can see them — and they are exactly what a background check tests. Before they existed, every date on a generated resume was supplied from source text with no provenance at all.
{
"employment_id": "EMP_PRIOR",
"employer": "Acme Retail Group",
"employer_of_record": "Contoso Contracting Ltd",
"title": "Senior Software Engineer",
"start": "2018-06", "end": "2022-02",
"parent_employment_id": null
}
employer_of_record is the entity that actually paid you, where it differs from
the name on your resume. A background check compares against that, not the client.
It never appears in an artefact; it exists so a reference check does not surprise
you.
parent_employment_id links a promotion to the role it grew out of, so a
progression can be collapsed to one line without losing the detail.
Education
Qualifications, held to the same standard as employment and for the same reason: a degree is not a STAR achievement, it is a fact a background check verifies.
{
"education_id": "EDU_MSC",
"institution": "Example University",
"qualification": "MSc",
"field": "Information Security",
"start": "2012-09", "end": "2013",
"grade": "Distinction"
}
Absent from the schema until 2026-09-06, which meant a degree could not be
recorded at all and export_resume_json.py could never fill JSON Resume's
education section. external_safe: false withholds a qualification from every
external document exactly as it does an atom.
Publications
Talks, keynotes, articles, books and book contributions, blog posts, reports, podcasts, videos, courses, software, datasets, and committee or board service. Recorded like education: a checkable fact with provenance, not a STAR story.
{
"publication_id": "PUB_BLACKHAT_2019_ATTACK",
"title": "MITRE ATT&CK: The Play at Home Edition",
"kind": "talk",
"venue": "Black Hat USA",
"date": "2019-08",
"url": "https://attack.mitre.org/resources/...",
"role": "co_speaker",
"collaborators": ["Katie Nickels"],
"employment_id": "EMP_PRIOR",
"evidence_id": "E_CONFERENCE_SPEAKING",
"source_refs": [{"source_id": "SRC_MITRE_ARCHIVE", "excerpt": "..."}],
"evidence_status": "externally_verified",
"external_safe": true
}
Added 2026-09-12 because a career page with "keynoted Black Hat, RSA and DEF CON"
as one achievement could not list the talks, and a resume cannot cite what the
pack does not itemise. evidence_id points at the achievement that summarises a
group of items, so the two never drift apart. Intake extracts every item it can
find in resumes, catalogues, author archives and programme pages; a public page
that lists the item makes it externally_verified, a subject-supplied catalogue
leaves it self_asserted. Roles use the same vocabulary as the record: a
foreword is not an author, a co_speaker is not a speaker.
Source records
Where material came from. Files carry a sha256 and the character count of
extracted text, not the file size. URLs carry a retrieved date instead.
A person source is a conversation: the subject's own answers during a review,
dated, pointing at the review record that holds them. It has no hash, so like a
URL its retrieved date is the only thing making it auditable. It exists because
an answer to a good question is evidence, and evidence with no provenance cannot
be told apart from evidence a persuasive question produced.
independent: true marks a source you did not write. Only atoms citing one can
reach externally_verified.
Skill vocabulary
Canonical skill names mapped to aliases, so "threat modelling" and "threat modeling" are one thing. Free-text skills fragment as a pack grows and quietly break recall; a search that silently misses things is worse than no search.
Private profile
Name, location, email, phone, LinkedIn. A document for a named recipient can use
contact details; a public document gets name and location. address and
photo_reference are stripped from application generation context. The private
guided review and lossless JSON export preserve the full recorded profile for
inspection and backup; the read-only pack_html.py overview omits contact details.
Capture notes
data/capture/notes.jsonl — an append-only log, deliberately not part of the
pack. A thirty-second note must not require a new pack version, because that
friction is what stops people capturing at all. Notes are promoted into atoms
later, ids are never reused.
Relationship to JSON Resume
JSON Resume (resume.json) is an established standard
with 14 top-level sections, ISO 8601 dates, and a theme ecosystem. Its schema has
no fields for evidence, provenance, sources, or confidence — it records what
you claim, not how you know it.
career.json is the layer above: it keeps the evidence, the sources, the
confidence, the dates you inferred rather than recorded, and the material you must
never publish. scripts/export_resume_json.py projects it down, dropping
everything JSON Resume cannot represent.
Project down for compatibility. Never treat the projection as the record: it has thrown away the parts that make the record trustworthy.
Scope and positioning (2026-09-07)
Two things every cold recruiter screen asked for that evidence atoms cannot hold.
employment[].scope: team size, direct reports, budget owned, organisation size, geography, with source refs. Background-check facts, like dates, so they live on the employment record and render on the role line.role profile.positioning: the level story in the subject's words: why this role now, what they want more of, what they are stepping away from. Dated and cited as a person source. When null, the summary states only what the evidence shows and never invents intent.
Supported strengths and future direction
Schema 1.4 adds optional strengths_profile and positioning_preferences arrays.
A strength has an interpretation, supporting atom IDs and fingerprints, timeframe,
limitations, status and interview state. Confirmation or rejection requires a
person-source answer. Changed supporting evidence makes the interpretation stale;
refresh fingerprints only after reassessment. Preferences record future intent,
not historical achievements. Neither record can promote an atom's evidence status.
Proposed and accepted career data
The career schema describes a pack's contents. Storage and review records determine whether that content is proposed or current:
| Record | Location | Purpose |
|---|---|---|
| Proposed pack | data/candidates/ |
Complete candidate preserving stable IDs and existing fields |
| Review snapshot | reviews/pack-reviews/<id>/proposal.json |
Exact content shown to the person |
| Review session | reviews/pack-reviews/<id>/session.json |
Hash pins for the proposal and previous pack |
| Decision batches | reviews/pack-reviews/<id>/decisions/ |
Explicit choices, reviewer name, explanations and omission notes |
| Accepted pack | data/packs/ |
Current/history chain resolved through metadata.supersedes |
| Acceptance receipts | metadata.human_review |
Accepted item fingerprints and decision-file pins |
The decision input contract is
pack-review-decisions.schema.json.
Human review binds exact wording and supporting records; it is independent from
evidence_status and external_safe. A receipt stops showing as accepted if its
content or decision-file pin changes. Reviewer names record attribution locally;
they are not an authentication mechanism.
Partial acceptance preserves existing unaccepted facts. Missing supporting records
or inconsistent references block a new version. Legacy schema issues may be shown
in a review, but must be corrected before saving a new accepted version. Capture
captured dates require YYYY-MM-DD or null; occurred supports less precise
career dates. Never infer a capture day just to pass validation.
See career-pack review for the save/resume flow. Output briefs and scoped selections belong to the optional Resume Application.