Guides/The career.json format

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_typeactivity (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.

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.