Skip to main content

Import format

This format holds a household snapshot. The example plan starts on 2026-02-02. Household names, stock, and dates are sample data. After import, the database holds the current state. This file is not a sync or backup format.

See Data import for commands and safety checks.

Ground rules​

  • References use names or keys, not database IDs. Ingredient references use name from ingredients[]; names are normalized before lookup. Recipes use key, which becomes recipe.slug. Cook-event keys are unique within a plan.
  • Field names mirror src/db/schema.ts columns in camelCase (weekStart, plannedServings, prepMinutes, qtyApprox, scalingExempt, …). Fields that only exist in the seed (key, household.members, plan on a shopping list) are marked below.
  • Dates are YYYY-MM-DD; timestamps are ISO-8601 UTC strings. Quantities are JSON numbers or null (unknown amount, e.g. "to taste" / "on hand").
  • Units are already normalized to the names accepted by normalizeUnit(): lb, oz, g, kg, ml, l, cup, tbsp, tsp, fl_oz, pint, quart, gallon, count and the package/count units can, jar, bag, bunch, head, clove, sprig, stalk, package, bottle, box, slice, piece. The loader still runs normalizeUnit() on every unit and aborts on an unknown one.
  • Enums match schema.ts exactly; see Enumerations. The loader validates every enum value and aborts with AppError("validation", …) before writing anything.
  • Every ingredient used anywhere in the file is declared in ingredients[]: recipe lines, pantry rows, shopping items, unit conversions, and ingredient-scoped rules. Unresolved names are a hard error ({ unresolved: [names] }), because the seed must not silently create shadow ingredients.

Shape​

{
"household": { … },
"ingredients": [ … ],
"unitConversions": [ … ],
"pantry": [ … ],
"rules": [ … ],
"recipes": [ … ],
"plans": [ … ],
"shoppingLists": [ … ]
}

All eight top-level keys are required; the arrays may be empty but are not omitted.

household (required, object)​

FieldTypeNotes
namestringhousehold.name.
timezonestringIANA zone, household.timezone.
settingsHouseholdSettingsStored as household.settings JSON; all fields required.
settings.defaultServingsnumberDefault servings when a recipe is planned without one.
settings.spiceLevelenum spiceLevelHousehold heat tolerance. Mild here, so heat is added after cooking.
settings.lunchBatchServingsnumberBatch-cooked lunch size (6 = 2 people × 3 days).
settings.dinnerServingsnumberDinner cook size (4 = cook once, eat twice).
settings.weekStartsOnnumber1 = Monday (ISO).
settings.unitSystem"us" | "metric"Display preference.
settings.planningNotesstring | nullFree-form planning guidance for agents.
membersarrayCreated in order as household_member rows.
members[].namestringMember display name.
members[].notesstring | nullFree-text dietary notes; structured bans live in rules.

ingredients[] (required)​

FieldTypeRequiredNotes
namestringyesCanonical display name; must be unique (case-insensitive). Referenced everywhere else by this string.
categoryenum ingredientCategoryyes
defaultUnitunit | nullyesUsed for stock/shopping when nothing else is specified.
aliasesstring[]yesMay be []. Lowercased by the loader; must not collide with any alias or ingredient name. Each becomes an ingredient_alias row.
descriptionstringnoOptional prose description.
imageUrlstring | nullnoIcon: a bundled icon name, an http(s) link, or null for automatic. See Ingredient icons.
shelfLifeDaysintegernoTypical shelf life once purchased. Shopping completion uses it to set the expiry date.
parQtynumbernoDesired minimum stock. Unset in this seed so imported quantities cannot flip a row to low.
parUnitunitnoUnit for parQty.

unitConversions[] (required)​

FieldTypeNotes
ingredientstring | nullIngredient name, or null/omitted for a household-global conversion.
fromUnitunit
toUnitunit
factornumber1 fromUnit = factor toUnit, stored as unit_conversion.factor. May be fractional. Must be > 0.
notestring | nullHuman explanation ("one 14.5 oz can").

Unique per (ingredient, fromUnit, toUnit). The service already handles the inverse direction, so only one direction is seeded. These rows are what make recipe units comparable to pantry units, e.g. can → oz for canned tomatoes, count → lb for chicken breast, head → clove for garlic, head → cup for broccoli, clove → tsp for minced garlic, bunch → sprig for dill.

pantry[] (required)​

One row per ingredient; the ingredient must be unique.

FieldTypeNotes
ingredientstringDeclared ingredient name.
qtynumber | nullUnknown when null. Read status as well.
unitunit | nullRequired when qty is non-null; null with qty: null.
qtyApproxbooleanTrue for parsed estimates (~2.5 lb, ~10 cloves).
statusenum pantryStatusExpected status after import. This seed uses on_hand everywhere.
rawTextstringFree-text snapshot line, stored verbatim for provenance.
notestring | nullParsing/context note ("amount not tracked").

rules[] (required)​

FieldTypeNotes
scopeenum ruleScopeingredient, category, recipe, tag, keyword.
ingredientstringRequired for scope: "ingredient"; must resolve.
recipestringRequired for scope: "recipe"; recipe key.
targetTextstringCategory name, tag, or keyword text. Keyword checks match whole words and ignore case.
severityenum ruleSeverityforbidden, avoid, limit.
conditionNotestringOptional judgment call the agent must apply.
reasonstringWhy the rule exists.

Rules are household-wide (memberId null) in this seed.

recipes[] (required)​

Top-level recipe fields:

FieldTypeNotes
keystringSeed identity; written to recipe.slug. Unique, URL-safe.
titlestring
descriptionstring | null
sourceUrlstring | nullnull for authored recipes.
sourceNamestring | nullSite/blog name.
authorstring | null
imageUrlstring | nullHero image.
servingsnumberYield the ingredient quantities are written for. Plan scaling is separate (plannedServings may differ).
yieldTextstring | nullHuman yield ("8 servings (about 12 cups)").
prepMinutesinteger | null
cookMinutesinteger | null
totalMinutesinteger | nullIncludes passive time when the source does.
spiceLevelenum spiceLevel | nullDish heat, not household preference.
cuisinestring | null
mealTypesenum mealType[]At least one.
tagsstring[]
equipmentstring[]
nutritionobject | null{ calories?, proteinG?, carbsG?, fatG?, fiberG?, sugarG?, sodiumMg? }, all numbers.
storageNotesstring | nullReheat/storage guidance.
leftoverDaysinteger | nullDays leftovers keep.
statusenum recipeStatusAll recipes here are want_to_try.
ratinginteger (1–5) | null
notesstring | nullHousehold adaptations, source caveats, "ingredients reconstructed" flags.
ingredientsarrayOrdered; loader sets position from array order.
stepsarrayOrdered; loader sets position from array order.

ingredients[] entries (written to recipe_ingredient):

FieldTypeRequiredNotes
ingredientstringyesMust resolve to a declared ingredient.
qtynumber | nullyesnull = "to taste"/unmeasured.
qtyMaxnumbernoUpper bound for ranges; qty ≤ qtyMax.
unitunit | nullyesnull iff qty is null.
prepNotestring | nullyes (may be null)"large, diced", "drained".
originalTextstringyesOriginal recipe line, kept for fidelity.
optionalbooleanno (default false)
scalingExemptbooleanno (default false)Salt/spices/to-taste items.
groupLabelstringnoSection header ("Sauce", "For the Salmon").

steps[] entries (written to recipe_step):

FieldTypeRequiredNotes
textstringyesFull instruction.
groupLabelstringnoSection header.
durationMinutesintegernoTimer hint.
linksarraynoIngredient lines that this step uses.
links[].ingredientIndexintegeryesZero-based position in the recipe's ingredients array. Must point to a line of the same recipe.
links[].qtynumber | nullnoAmount in the linked line's unit. null means the whole line.

The unit of a link is always the unit of its ingredient line. Written to recipe_step_ingredient.

plans[] (required)​

FieldTypeNotes
weekStartdateFirst day of the plan week; follows household settings. Unique per household.
titlestring | null
statusenum planStatusactive here.
notesstring | null
cookEventsarrayCreated before slots/prep tasks so references resolve.
slotsarray
prepTasksarray

cookEvents[]:

FieldTypeNotes
keystringUnique within the plan; referenced by slots[].cookEvent and prepTasks[].cookEvent.
recipestringRecipe key.
cookDatedateMay fall outside the plan week (Sun Feb 1 for the Feb 2 week; overnight oats are dated the evening before the breakfast).
plannedServingsnumberMay differ from recipe.servings; services scale.
labelstring | null"Batch A - lunch (Mon-Wed)".
statusenum cookStatusAll planned; nothing has been cooked yet.

slots[]:

FieldTypeNotes
datedate
mealTypeenum mealType
kindenum slotKindmeal for a planned meal, freeform for "Leftovers / …".
cookEventstringRequired iff kind == "meal"; forbidden for freeform/skip. Slots dated after the cook are leftovers — no separate leftover kind.
servingsnumberServings eaten in this slot (omitted/null for freeform).
labelstring | nullDisplay text for freeform slots.
notesstring | null"Leftovers from Monday."

prepTasks[]:

FieldTypeNotes
dueDatedate
descriptionstring
cookEventstringOptional link; omitted when the task is not tied to a specific cook event.

shoppingLists[] (required)​

FieldTypeNotes
titlestring
plandate | nullweekStart of the owning plan; resolves to meal_plan_id.
statusenum shoppingListStatusdone here.
completedAtISO timestamp | nullSet on the list.
notesstring | nullProvenance notes.
itemsarrayOrdered; loader sets position.

items[]:

FieldTypeNotes
ingredientstringLinked item: resolves to an ingredient id.
namestringFree-text item when ingredient is absent; defaults to the ingredient name.
categoryenum ingredientCategoryAisle grouping; defaults to the ingredient category.
qtynumber | null
unitunit | null
purposeNotestring | null"chili 2 lb + beef & broccoli 1 lb".
checkedbooleantrue → checkedAt = list.completedAt, actualQty/actualUnit default to qty/unit.
notestring | null

Loader semantics​

Validation and writes​

The CLI checks the JSON and schema before it creates a household. The loader then checks existing data and resolves references.

Food data, household settings, and members load in one transaction through the service layer. A failed load rolls back that transaction. A household created by the CLI before the load can remain.

The load refuses a household that has ingredients or recipes. --force skips that guard; it does not replace or merge data.

Ingredients load before recipes. Recipes load before rules and plans. Shopping lists load last.

Pantry history​

Each pantry row produces exactly one stock_movement: kind = "set", reason = "import", refType = "seed", refId = null, quantity/unit from the row (null allowed), note from the row. This is a snapshot import, not purchasing history; do not fabricate purchase movements.

Shopping lists​

The single list is historical (status: "done", completedAt set) and was bought on 2026-02-01 after the pantry was counted. The pantry snapshot in pantry[] already includes these purchases, so the loader:

  • creates the list and items with source: "manual";
  • sets checkedAt = completedAt, actualQty = qty, actualUnit = unit for checked: true;
  • does not create purchase movements and does not adjust stock. Re-running purchase math would double-count everything.

The "Already on hand" section and the "Salt, black pepper — on hand" line are intentionally absent from the items (nothing was bought), and the two summary pantry lines ("Common spices", "Tomatoes") are absent from pantry[].

Timestamps and ids​

createdAt/updatedAt come from ctx.now() (or DB default) during import; completedAt comes from JSON. Ids are generated by newId(prefix) using the prefixes in the contract (hh, mem, ing, ali, uc, mv, rcp, ri, rs, rse, rule, plan, ce, slot, prep, sl, sli). The seed does not encode timestamps for individual rows.

Notes on this example seed​

  • Identity fields are synthetic: household Example Household, members Robin and Sam, timezone UTC, and the week starting 2026-02-02. Stock levels and pantry notes are randomized; they are plausible but not derived from any real household.
  • rawText is regenerated as <ingredient> | <qty> <unit> | <category> | 2026-02-01, or <ingredient> | on hand | … for unknown amounts. It is provenance text, not parsed by the loader.
  • on hand → qty: null, unit: null, status: "on_hand"; every pantry row is on_hand because no row sets parQty.
  • Recipes: the lunch/dinner recipes are public blog recipes transcribed with their ingredients, steps, times, servings, image and nutrition. Breakfasts are authored (no source URL). Plan-level adaptations live in each recipe's notes, not in the ingredient quantities.
  • Rules encode the household preferences (mushrooms; pork/bacon/sausage/ham/prosciutto/pancetta; seafood except tuna/salmon; mayonnaise only in a minor role; beef only as ground beef). "Keep it mild" is a household setting (spiceLevel: "mild"), not a rule.
  • All planned meals are cook_status = planned; no cook event has happened yet.
  • Recipe and shopping quantities describe the example plan's math; pantry stock is deliberately independent of them.

Contents of this seed​

SectionCount
household members2
ingredients65
unit conversions16
pantry rows46
rules11
recipes8 (5 sourced + 3 authored breakfasts)
plans1 (week 2026-02-02, 12 cook events, 21 slots, 7 prep tasks)
shopping lists1 (33 done items)

Enumerations​

Copied from src/db/schema.ts for quick reference; the schema is the source of truth.

  • ingredientCategory: produce, protein, dairy, bakery, pantry, baking, spice, canned, condiment, frozen, beverage, other
  • pantryStatus: on_hand, low, out, unknown
  • movementKind: set, delta
  • movementReason: purchase, cook, waste, adjust, import, verify
  • movementRefType: cook_event, shopping_list, manual, seed
  • recipeStatus: want_to_try, rotation, never, archived
  • mealType: breakfast, lunch, dinner, snack, dessert
  • spiceLevel: none, mild, medium, hot
  • ruleScope: ingredient, category, recipe, tag, keyword
  • ruleSeverity: forbidden, avoid, limit
  • planStatus: draft, active, archived
  • cookStatus: planned, done, skipped
  • slotKind: meal, freeform, skip
  • shoppingListStatus: draft, shopping, done
  • shoppingItemSource: generated, manual