Brewlang v0 specification
This page defines Brewlang v0 for anyone writing an implementation, an editor mode or a converter. The guide explains the same language for recipe writers. The reference implementation is the brewlang TypeScript package.
Brewlang is at v0: the syntax can still change. Changes are listed in the brewlang repository’s history until a versioning policy is set for v1.
Brewlang describes recipes made of pours and gestures over time: pour-over (V60, Kalita, Melitta, Chemex, Origami, UFO, Phin), immersion (AeroPress, French press, siphon) and hybrid valve brewers (Switch, Clever, Pulsar). Out of scope: espresso and moka pot, driven by pressure and heat rather than pours; cold brew, which needs durations in hours.
A recipe is a UTF-8 text file with the .brew extension. Lines end with LF or CRLF. Each line holds one element; blank lines are allowed anywhere.
Grammar
Section titled “Grammar”The grammar gives the form; the semantic rules give the meaning. WS is one or more spaces or tabs between tokens.
file = [ frontmatter ] { blank NL } header { line } ;
frontmatter = "---" NL { yaml_line NL } "---" NL ;header = brewer WS dose [ WS water ] [ WS temp ] [ WS comment ] NL ;line = ( grind | pour_step | action_step | temp_change | target | comment | blank ) NL ;
grind = "grind" WS grind_size [ WS comment ] ;pour_step = [ time WS ] pour_water { WS pour_mod } [ WS comment ] ;action_step = [ time WS ] action [ WS duration ] [ WS comment ] ;temp_change = temp [ WS comment ] ; (* alone on its line, no time *)target = "target" WS time [ WS comment ] ;
brewer = "@" ident ;action = "/" ident ;dose = amount ( "g" | "oz" ) ;water = amount water_unit ;pour_water = [ "+" ] amount water_unit ; (* no '+': total on the scale; '+': water to add *)temp = amount ( "°C" | "°F" ) ;water_unit = "g" | "ml" | "oz" | "floz" ;pour_mod = duration | qualifier ; (* in any order, at most one duration *)qualifier = "bloom" | "spiral" | "center" | "pulse" ;grind_size = "extra-fine" | "fine" | "medium-fine" | "medium" | "medium-coarse" | "coarse" | "extra-coarse" ;
duration = "~" ( number ( "s" | "m" ) | time ) ; (* ~15s, ~4m, ~3:30 *)time = digit { digit } ":" digit digit ; (* seconds under 60 *)amount = number [ "-" number ] ; (* a value or a range *)number = digit { digit } [ "." digit { digit } ] ;ident = letter { letter | digit | "-" } ;comment = "--" { any_char } ;Every token identifies itself: n:nn is always a time, a number is always followed by its unit with no space, and a ratio is never written. Keywords (grind, target), brewer names and units are matched as written, except brewer names, which are matched ignoring case.
Semantic rules
Section titled “Semantic rules”Each rule reports a diagnostic with a line and a column. Errors make the recipe invalid; warnings point out a probable mistake; suggestions point out a possible improvement. An implementation runs the semantic rules only on a recipe without syntax errors.
- Error: more than one
grindline. - Error: a
grindline after the first step. Comments may come before it.
- Error: a timed step that starts before the previous timed step. Equal times are allowed and run in the written order.
- Untimed steps before the first timed step are preparation, done in the written order before starting the timer. Untimed steps after it go at the user’s pace. A recipe without any time runs step by step, and the timing rules do not apply.
- Suggestion: an action at
0:00before the first pour. It is a preparation; its time can go.
- The running total starts at 0. A pour without
+sets it; a pour with+adds to it. - Error: a pour without
+that is not above the running total. - Error: a pour with
+that adds nothing. - Error: the header announces a total water different from the running total after the last pour. Ranges compare bound by bound. Without pours, the header water is the water to use.
- Suggestion: a recipe mixing both forms of pour, on the first pour whose form differs.
- Suggestion: the first pour is not marked
bloom, is at most 3 times the dose in the same unit, and another pour follows. - Error: a timed pour whose duration goes past the start of the next timed step.
- Error: a water amount in a different unit from the first one (header included).
- Error: a temperature in a different unit from the first one.
- Error at parse time: an unknown unit. A misspelled temperature (
94C,94°c) suggests the correct one; spoons and cups are unknown units.
- Error at parse time: an unknown qualifier, with a correction when it is one letter away from a known one (
blom→bloom). A known action name written as a qualifier (swirl) suggests writing it as an action on its own line. - Error: more than one technique (
spiral,center,pulse) on a pour.bloommay appear on any number of pours.
Actions
Section titled “Actions”- Any action name is accepted.
- Warning: an unknown name within one edit of a known action, or two edits for names longer than 5 letters (insert, delete, replace, or swap two neighbors). The message proposes the known name.
- Error: a core action used with a known brewer of a type it does not support. An unknown brewer has no restriction.
- Aliases are equivalent to their action:
/plungeis/press.
Brewer state
Section titled “Brewer state”The registry gives some brewers a starting state by convention.
- Valve brewers:
@Switchand@Pulsarstart open,@Cleverstarts closed. Warning: an/openor/closethat does not change the valve. When it is the first valve action, the message explains the starting state. - AeroPress: starts upright. Warning:
/invertwhen already inverted,/flipwhen upright,/presswhile inverted.
Temperature
Section titled “Temperature”- The current temperature is the header’s, until a temperature line changes it.
- Warning: a temperature line with the current value.
- Warning: a temperature line followed by another one before any step.
- Warning: a temperature line before the first pour or action; its value belongs in the header.
Target
Section titled “Target”- Error: more than one
target. - Warning: a target earlier than the start of the last timed step.
- A missing target is never deduced.
Metadata
Section titled “Metadata”Free key: value lines; no key is required. Without title, tools use the file name. filter is free text. Tools reading the metadata as data use only simple key: value lines.
Registry
Section titled “Registry”The brewers, their types, their starting states, the core actions and the types each one supports are listed in the vocabulary. The registry has one entry per brewing method, not per product.
These operations are part of the reference implementation. Another implementation that offers them must give the same results.
- Formatting rewrites a recipe in its canonical form without changing its meaning.
- Scaling multiplies the dose, the header water and every pour by one factor. Times, durations, temperatures and the grind are unchanged, with a warning. Amounts round to 1 g or ml and to 0.1 oz or floz;
+pours are derived from the rounded running total. - Conversion changes units within a kind: g ↔ oz for the dose and water weights, ml ↔ floz for water volumes, °C ↔ °F. Never weight ↔ volume. Rounding as for scaling, temperatures to 1 degree.
- JSON output follows the JSON format and its schema.
Conformance
Section titled “Conformance”The repository’s tests/canonical/ folder holds the recipes every implementation must agree on:
| Folder | Content |
|---|---|
valid/ |
Recipes with no diagnostic |
invalid/, warnings/, suggestions/ |
Each .brew with its expected diagnostics (severity, message, line, column) in a .json next to it |
scaling/ |
The scaling in the .json, and the scaled recipe in a .expected.brew |
json/ |
The expected JSON output in the .json |
Messages are part of the interface: they always say what to do, not only what is wrong.