Writing Gram programmatically
This page is for anything that generates .gram files rather than a person typing them by hand — an import script, a scraper, or an AI model converting an existing recipe (a website, a cookbook scan, a JSON-LD blob) into Gram. The official gram import command feeds the AI model it calls a system prompt (packages/cli/src/prompts/gram-spec.ts) that’s kept manually in sync with this guidance, so treat this page as the published, human-readable version of that same knowledge.
Restructure, don’t transliterate
Section titled “Restructure, don’t transliterate”Traditional recipe text contains many steps that only exist because prose has no other way to express them. In Gram, most of these dissolve into the syntax itself instead of staying separate steps:
- “Cut the lemon in half” → just
@lemon{1}(cut in half)on the ingredient that uses it. - “Reserve the seasoning” → just
&seasoningwhen it’s referenced later. - “Set aside and let cool” → just
~_{30min}at the end of the step that produces it.
A well-generated .gram file often has fewer steps than its source, different section boundaries, and a different logical flow — a 10-step source recipe might reasonably compile down to 5 Gram steps. Preserving the original step count is not a goal; clarity is. For every source step, the question worth asking is: does this step do real cooking work, or does it only exist to set up information that Gram can express inline? If it’s the latter, absorb it into the ingredient or timer that uses it instead of keeping it as its own step.
A conversion checklist
Section titled “A conversion checklist”When turning an existing recipe into .gram, working through these five points in order catches most structural mistakes before they happen:
- Structure — Identify distinct phases (dough, filling, sauce, assembly…). Each phase with its own ingredient list is a candidate for a
## Section. Anything that must be prepared in advance gets a retro-planning lead time (see Document Structure). - Frontmatter — Extract
title,author,source,category,portions, and dates from whatever metadata the source provides. Never invent a value that isn’t in the source. - Ingredients — List every unique
@ingredientusing its full name with spaces ("olive oil", not"olive-oil"). Note which ones appear more than once — the second and later mentions should use@&. Prefer SI units for precision, but keeptbsp/tsp/cupfor small amounts when that’s how the source expresses them. For parts of an ingredient (juice, zest, yolk…), plan the composite syntax instead of a separate prep step. - Steps — Write each step as a paragraph with a
[Verb]action prefix, inlining every@ingredient,#cookware,~timer, and^temperatureas it’s introduced. Use passive timers (~_) for hands-off waits (oven, resting, chilling, rising) and active timers (~) for anything that keeps the cook busy. If multiple passive timers must happen sequentially (one after another, e.g. baking in stages), give them the SAME NAME (~_baking{10m}then~_baking{30m}) to automatically sequence them without adding to the cook’s active time. - Review — Before finalizing, check for the mistakes listed below: kebab-case names, units inside cookware braces, missing
@&on repeat mentions, redundant->&namedeclarations, and standalone steps that only prepare an ingredient for a later step.
Common mistakes
Section titled “Common mistakes”These are the errors that show up most often in programmatically generated .gram files.
Kebab-case ingredient names
Section titled “Kebab-case ingredient names”Gram ingredient names are real words with spaces, not slugs. Kebab-case is an internal, database-level concept — never syntax.
Multi-word names without {}
Section titled “Multi-word names without {}”A multi-word @ingredient/#cookware name always needs {} (or {count} for cookware) as its closing delimiter — even when the exact quantity is unknown, use empty braces {}. There is no bare (no-{}) form for a multi-word name; the bare form only works for a single word (@salt, #pan). Without {}, only the first word becomes the name and everything after it silently becomes ordinary step text — inside an alternative (|) this now throws a hard parse error instead of silently corrupting the group.
Units inside cookware braces
Section titled “Units inside cookware braces”#cookware braces accept integer counts only. Dimensions, materials, and descriptions always go in parentheses — see Cookware.
Free text where a number is required
Section titled “Free text where a number is required”Quantities and timers need a real number (or empty braces for “to taste”); fuzzy text doesn’t parse.
Using @& on the first occurrence
Section titled “Using @& on the first occurrence”Confusing @& (raw ingredient) with & (intermediate)
Section titled “Confusing @& (raw ingredient) with & (intermediate)”See Intermediate Variables for the full distinction. More generally, whenever a step transforms an ingredient into a new product referenced by name in later steps (“the seasoned chicken”, “the dough”), declaring it with ->&name avoids this ambiguity entirely.
Standalone steps that only prepare an ingredient
Section titled “Standalone steps that only prepare an ingredient”Any step whose entire purpose is “take X and do Y to it before the real step” should collapse into the ingredient reference of the step that actually uses it, using the () preparation shorthand — here it attaches to the composite’s parent (<@lemon{1}(...)), since “cut in half” describes the lemon, not the juice.
“The juice/zest/part of” phrasing instead of composite syntax
Section titled ““The juice/zest/part of” phrasing instead of composite syntax”Composite ingredients keep the shopping list accurate (the overlap rule: zest + juice from the same lemon still aggregates to one lemon) and remove the need for a standalone prep step. Always give the child its full name (parent word included), not a bare generic word like @juice — a bare child name becomes its own entry in the shared ingredient database with no memory of which parent it came from, so “juice” from a lemon and “juice” from an orange elsewhere would wrongly collide into one database entry. See Naming the child for the full reasoning.
Spaces around the composite operator
Section titled “Spaces around the composite operator”Attaching a composite’s preparation to the wrong side
Section titled “Attaching a composite’s preparation to the wrong side”Both the child and the parent of a composite can carry their own independent () preparation:
- Child’s preparation goes right after its own name (and quantity, if any).
- Parent’s preparation goes right after
<@parentName{parentCost}.
Attach it to whichever one it actually describes — “cut in half” happens to the whole lemon, not to the extracted juice. They can also combine when both genuinely need one: @lemon juice{150ml}(strained)<@lemon{1}(cut in half).
A redundant ->&name when the section title already declares one
Section titled “A redundant ->&name when the section title already declares one”A section-level ->&name already captures the output of every step inside it — see Intermediate Variables.
An active timer for passive cooking
Section titled “An active timer for passive cooking”A // comment in the middle of a step
Section titled “A // comment in the middle of a step”// comments to the end of the line, so anything after it — including
ingredients — is silently dropped. The file still compiles, with no warning, and
is simply missing them.
Inside a step, use a block comment. Keep // at the very end of a line, or on
a line of its own. gram import checks for this specific loss and refuses a
result where it happened.