Data Formats
The JSON shapes that flow between pipeline stages, and the YAML schema for the ingredient database. This page complements the per-package references — it's the "what does the data actually look like" companion to their "what functions exist" content.
1. The AST (@gram-lang/parser)
For this source:
---
title: 'Crepes'
---
## Batter
Mix @flour{200g} and @milk{200ml}.getAST() returns (annotated, loc offsets omitted for brevity — every node except RecipeAST itself carries one):
{
"type": "Recipe",
"meta": { "title": "Crepes" },
"children": [
{
"type": "Section",
"title": "Batter",
"retroPlanning": null,
"intermediateDecl": null,
"children": [
{
"type": "Step",
"action": null,
"children": [
{ "type": "Text", "value": "Mix " },
{
"type": "Ingredient",
"name": "flour",
"modifiers": [],
"alias": null,
"preparation": null,
"composite": null,
"quantity": {
"type": "Quantity",
"value": { "type": "single", "value": 200, "text": "200" },
"unit": "g",
"fixed": false
}
},
{ "type": "Text", "value": " and " },
{
"type": "Ingredient",
"name": "milk",
"modifiers": [],
"quantity": {
"type": "Quantity",
"value": { "type": "single", "value": 200, "text": "200" },
"unit": "ml",
"fixed": false
}
},
{ "type": "Text", "value": "." }
]
}
]
}
]
}See parser.md for the full set of node interfaces and the ASTNodeType enum.
2. Compiled & analyzed recipes (@gram-lang/kitchen, @gram-lang/analyzer)
compile() produces a CompilationResult; analyze() returns that same shape enriched with mass/nutrition fields (AnalyzedCompilationResult). Diffed below — analyzer-only fields are marked:
{
"title": "Crepes",
"slug": "crepes",
"meta": { "title": "Crepes" },
"registry": {
"ingredients": {
"flour": { "id": "flour", "name": "flour" },
"milk": { "id": "milk", "name": "milk" }
},
"cookware": {}
},
"shopping_list": [
{
"id": "flour",
"name": "flour",
"qty": 200,
"unit": "g",
"_usageIds": ["1"],
"normalizedMass": 200, // analyzer
"conversionMethod": "physical", // analyzer
"isEstimate": false, // analyzer
"bakersPercentage": 100 // analyzer, only if baker's math is active
},
{
"id": "milk",
"name": "milk",
"qty": 200,
"unit": "ml",
"_usageIds": ["2"],
"normalizedMass": 206, // analyzer — converted via density if known
"conversionMethod": "density", // analyzer
"isEstimate": true // analyzer — density came from the ingredient DB, not an explicit override
}
],
"cookware": [],
"sections": [
{
"title": "Batter",
"retro_planning": null,
"ingredients": [ "/* same Usage objects as shopping_list, enriched identically */" ],
"cookware": [],
"steps": [
{
"type": "step",
"timings": { "start": 0, "end": 2, "activeDuration": 2 },
"backgroundTasks": [],
"content": [
"Mix ",
{ "id": "flour", "_usageId": "1", "qty": 200, "unit": "g", "normalizedMass": 200 },
" and ",
{ "id": "milk", "_usageId": "2", "qty": 200, "unit": "ml", "normalizedMass": 206 },
"."
]
}
],
"metrics": { // analyzer, per-section
"totalMass": 406,
"massStatus": "estimated",
"missingMassIngredients": []
}
}
],
"warnings": [],
"metrics": {
"preparationTime": 2,
"cookTime": 0,
"activeTime": 0,
"totalTime": 2,
"totalMass": 406, // analyzer, global
"massStatus": "estimated", // analyzer
"missingMassIngredients": [], // analyzer
"nutrition": { // analyzer, only if enableNutritionalEstimation
"total": { "calories": 748, "protein": 27.7, "carbs": 130.4, "fat": 11.4 },
"isEstimate": true,
"coverage": 1
}
}
}Note the compiler's StepToken vocabulary inside content: plain narrative text is a bare string; ingredients/cookware/references share the Usage shape (no type field, identified by having an id); timers/temperatures/comments/declarations each carry their own lowercase type. This is intentionally distinct from the parser's PascalCase ASTNodeType — it describes compiled output, not parsed input. See How to Build a Custom UI for a walkthrough of consuming this shape in a frontend framework.
3. Ingredient database (YAML)
The database passed to validateIngredientDatabase() / analyze() is a flat Record<string, IngredientData> keyed by ingredient slug. The gram CLI additionally accepts (and unwraps) an optional top-level ingredients: key, so both of these are valid .gram/ingredients.yaml files:
# With the optional wrapper (what `gram init` scaffolds)
ingredients:
flour:
name: "All-purpose flour"
aliases: ["farine"]
category: "Grains"
physical:
density: 0.59 # g/mL — bridges volume units ("1 cup") to mass
yield: 1.0 # fraction (0, 1] — net/gross ratio after prep waste
unit_weight: 120 # grams per "unit" (e.g. "1 flour" if ever used bare)
nutrition: # all values per 100 g
calories: 364
protein: 10.3
carbs: 76.3
fat: 1.0
sugar: 0.3
fiber: 2.7
sodium: 2
# Flat form (what validateIngredientDatabase()/analyze() expect directly)
flour:
name: "All-purpose flour"
physical:
density: 0.59physical and nutrition are both optional — an entry with neither is still valid (it just contributes no mass/nutrition data, surfacing as missingMassIngredients / a MISSING_MACROS warning). Only name is required. nutrition.calories/protein/carbs/fat are required whenever nutrition is present; sugar/fiber/sodium/sat_fat/mono_fat/poly_fat/alcohol are all optional.