🍳 Foodlang

The .food format

.food is the fun, hand-written authoring format for Foodlang — a tiny recipe you write like a little script.

Recipes humans can write. Machines can understand.

Two formats, one recipe

.food       = fun human authoring format
.food.yaml  = official ORF-compatible structured output

Write this:

recipe "Iced Vanilla Latte" {
  ingredients {
    espresso 2 shots
    milk 10 oz
    vanilla_syrup 20 ml
    ice 1 serving
  }

  steps {
    add vanilla_syrup
    add espresso
    stir 5 sec
    add ice
    pour milk
  }
}

Compile to this:

recipe_name: Iced Vanilla Latte

ingredients:
  - espresso:
      amounts:
        - amount: 2
          unit: shots

steps:
  - step: Add vanilla_syrup.

Foodlang KDL is easy to write. Foodlang YAML is easy to integrate.

Nodes

A document has exactly one top-level recipe or drink node, with an ingredients block and a steps block.

recipe "Name" {
  ingredients {
    NAME AMOUNT UNIT    // e.g. espresso 2 shots
  }
  steps {
    ACTION ...          // e.g. stir 5 sec
  }
}
Inside ingredients NAME AMOUNT UNIT — e.g. milk 10 oz
Inside steps ACTION ... — e.g. pour milk, stir 5 sec
yield AMOUNT UNIT optional, top-level
description "..." optional
tag NAME optional, repeatable

yield is optional — most drinks don't need it.

Phases (instead of steps)

For process-y recipes (espresso, brewing) you can use a phases block instead of steps. A phase is ACTION SUBJECT [to OUTPUT] { ITEMS }, where items are KDL child nodes: key value unit parameters (e.g. pressure 2 bar), bare references (vanilla_syrup), or verb measure sub-actions (stir 5 s), separated by newlines or ;. Like the rest of .food, phases are valid strict KDL — units are separate tokens, not glued:

drink "Iced Vanilla Latte" {
  ingredients {
    coffee 18 g
    vanilla_syrup 20 ml
    milk 10 oz
    ice 140 g
  }

  phases {
    preinfuse coffee { pressure 2 bar; flow 2 mlps; time 6 s }
    brew coffee to shot { pressure 9 bar; stop 40 g; max 28 s }
    build cup { vanilla_syrup; shot; stir 5 s; ice; milk }
  }
}

Phases map to an X-Phases block in .food.yaml and feed the execution plan (so machine targets like decent and s88 see the pressures and yields). A recipe uses either steps or phases.

Steps become sentences

The action verb leads; the rest is turned into readable text.

Step Sentence
add vanilla_syrup Add vanilla_syrup.
stir 5 sec Stir for 5 seconds.
grill 3 min each_side Grill for 3 minutes each_side.
whisk matcha water Whisk matcha water.

A leading <number> <time-unit> (sec, min, hr, …) becomes for <number> <unit(s)>.

Valid vs. invalid units

Because Foodlang uses standard KDL, amount and unit must be separate arguments — never glue them together.

✅ Use:

ingredient milk 10 oz
step stir 5 sec
ingredient espresso 2 shots

🚫 Do not use:

ingredient milk 10oz
step stir 5s
espresso 2shots

Keep it clean

The whole point is that .food reads like a tiny recipe script. Avoid property soup:

// avoid
ingredient "milk" amount=10 unit="oz"
step "pour" item="milk" to="cup"

// prefer
ingredient milk 10 oz
step pour milk

Validation

foodlang validate recipe.food          # friendly, forgiving
foodlang validate recipe.food --strict # unknown nodes become errors

Rules: exactly one recipe/drink node with a string name; at least one ingredient and one step; ingredients need name + amount + unit; yields need amount + unit; steps need at least one argument. Unknown nodes warn by default, and error under --strict.

Compile

foodlang compile recipe.food --target orf         # ORF-style YAML
foodlang compile recipe.food --target json        # normalized AST
foodlang compile recipe.food --target cooklang
foodlang compile recipe.food --target schema-org

Try it live in the Playground — switch the example to a .food file and compile.