For AI agents

The short version of the spec, for a model asked to produce a coffee recipe as structured data. Every example below is validated against the published schema in CI, so it cannot drift.

Prefer to read it all at once? /llms.txt is the link index and /llms-full.txt is every chapter concatenated.

Working inside a coding agent? coffeejson-org/skills packages the format as three agent skills — changing it, adding it to a product, and turning a published source into a document — installable with npx skills add coffeejson-org/skills.

The loop that matters

  1. Emit a document.
  2. Validate it against the authoring schema: coffeejson-1.0.authoring.schema.json.
  3. Fix what it rejects, and validate again.

Use the authoring schema, not the runtime one. The runtime schema is deliberately permissive — a consumer must ignore members it does not recognize, which means a misspelled key silently disappears instead of failing. The authoring schema sets additionalProperties: false, so the same typo is a loud error while you can still fix it. The one exception is the reserved ext member, which it admits anywhere for your own vendor data. Humans can paste into the validator, which runs entirely in the browser.

System prompt

Drop this into the system prompt of a model you are asking for recipes:

When asked to produce a coffee recipe as structured data, emit a CoffeeJSON document.

Rules:
- Top level is an object with "coffeejson": "1.0" and a "recipes" array (and/or "beans").
- Every quantity is an object: { "value": <number>, "unit": "<canonical unit>" }.
  Canonical units are "gram", "ounce", "celsius", "fahrenheit", "bar". Never a bare number, never a display symbol like "g" or "°C".
- Steps are ordered. "at_s" is seconds from brew start, cumulative — not a duration.
- "to_water" is the CUMULATIVE water total at that step, not the amount added.
- Omit anything you do not know. Do not invent a value to fill a field.
- Validate against https://coffeejson.org/schema/authoring/1.0
  and fix what it rejects. That schema refuses unknown keys, so a typo fails loudly.
- The one key it still accepts is "ext", the reserved home for your own vendor data.

Examples

15 g coffee, 250 g water, V60.

The floor. Only what was actually stated — no invented temperature, grind, or steps.

{
  "coffeejson": "1.0",
  "recipes": [
    {
      "title": "Everyday V60",
      "method": "pour_over",
      "coffee": {
        "value": 15,
        "unit": "gram"
      },
      "water": {
        "value": 250,
        "unit": "gram"
      }
    }
  ]
}

20 g coffee, 300 g water at 93 °C. Bloom with 60 g for 45 s, then pour to 300 g by 1:30.

Note `to_water` is cumulative: the second pour reads 300, not 240. And `at_s` is the clock time the step starts, not how long it lasts.

{
  "coffeejson": "1.0",
  "recipes": [
    {
      "title": "Two-pour V60",
      "method": "pour_over",
      "coffee": {
        "value": 20,
        "unit": "gram"
      },
      "water": {
        "value": 300,
        "unit": "gram"
      },
      "water_temp": {
        "value": 93,
        "unit": "celsius"
      },
      "steps": [
        {
          "kind": "bloom",
          "at_s": 0,
          "to_water": {
            "value": 60,
            "unit": "gram"
          },
          "instruction": "Bloom and swirl."
        },
        {
          "kind": "pour",
          "at_s": 45,
          "to_water": {
            "value": 300,
            "unit": "gram"
          },
          "instruction": "Pour in slow spirals."
        }
      ]
    }
  ]
}

A washed Ethiopian from Onyx, light roast.

A bean with no recipe is a complete document — `beans` and `recipes` are independent. Note `origin` is an object wrapping an `items` array (a blend is still ONE bean), and `country` is an ISO 3166-1 alpha-2 code.

{
  "coffeejson": "1.0",
  "beans": [
    {
      "name": "Ethiopia Washed",
      "roaster": {
        "name": "Onyx Coffee Lab",
        "type": "organization"
      },
      "roast_level": "light",
      "origin": {
        "type": "single",
        "items": [
          {
            "country": "ET",
            "process": [
              "washed"
            ]
          }
        ]
      }
    }
  ]
}

Mistakes models actually make

WrongRightWhy
"coffee": 15"coffee": { "value": 15, "unit": "gram" }A bare number has no unit. The format never infers one from locale.
"unit": "g""unit": "gram"Units are canonical ids, not display symbols. `g` and `°C` are rejected.
"to_water": 60 // meaning "add 60 g here""to_water": { "value": 260, "unit": "gram" }`to_water` is the cumulative total in the vessel at that step, not the increment added.
"at_s": 30 // meaning "this step takes 30 s""at_s": 90`at_s` is seconds from the start of the brew — when the step begins.
"grind": "medium-fine""grind": { "size": "medium_fine" }Grind is a structured object, and vocabulary values are snake_case.
"origin": [{ "country": "Ethiopia" }]"origin": { "items": [{ "country": "ET" }] }`origin` is an object wrapping `items` — a blend is still one bean — and `country` is an ISO 3166-1 alpha-2 code, not a name. (Both of these were caught by the schema while writing this page.)

Two rules worth repeating

Omit what you do not know. A document that says less is correct; a document that invents a water temperature is wrong, and it is wrong in a way no validator can catch. If the source did not state it, leave it out.

Unknown members are ignored, never rejected. That is what makes the format safe to extend, and it is why you should never write a consumer that fails on a key it does not recognize. See Versioning & conformance.

License and crawling

The spec prose, the schema, the fixtures, the registries, and the recipe corpus’s structure and transcription are CC0 — public domain. You may quote, reproduce, and build on them freely, with no attribution required and no conditions attached. Quoted roaster prose inside corpus documents remains the quoted source’s, carried as attributed quotation.

Separately from the license, this website’s robots.txt asks bulk training crawlers not to fetch it, while welcoming search and retrieval. That is a request about this server, not a term of the license: a CC0 artifact carries no usage restriction, and nothing here adds one.