Specification

Soustack Recipe Schema v0.2 — A portable, scalable, interoperable recipe format with first-class image support for both recipes and individual steps.

A Soustack document is a JSON object conforming to the JSON Schema specification. The schema defines a structured format for representing recipes with support for scaling, timing, equipment, storage, and substitutions.

Version 0.2.0 introduces normalized image fields so recipe and instruction imagery can be shared consistently across tooling. Older documents remain valid, but implementations should upgrade to take advantage of the image metadata outlined below.

Required Fields

Every recipe must include:

  • name — The title of the recipe (string)
  • ingredients — Array of ingredients (see below)
  • instructions — Array of instructions (see below)

Core Fields

Recipe Fields

Core document fields exposed at the recipe root:

FieldTypeRequiredDescription
namestringYesHuman-friendly recipe title.
descriptionstringNoShort summary of the dish.
categorystringNoClassification such as “Dessert” or “Snack”.
tagsstring[]NoKeywords for search and filtering.
imagestring | string[]NoOne or more URLs that represent the recipe visually.

Basic Information

  • id — Unique identifier, slug or UUID (string, optional)
  • name — Recipe title (string, required)
  • version — Semantic versioning, e.g., "1.0.0" (string, optional)
  • description — Recipe description (string, optional)
  • category — Recipe category, e.g., "Main Course", "Dessert" (string, optional)
  • tags — Array of tag strings (array, optional)
  • image — Image URL or array of URLs (string | string[], optional)
  • dateAdded — ISO 8601 date-time (string, optional)

Source Attribution

"source": {
  "author": "Jane Doe",
  "url": "https://example.com/recipe",
  "name": "Cookbook Name",
  "adapted": true
}

Yield

What the recipe produces. Required fields: amount and unit.

"yield": {
  "amount": 4,
  "unit": "servings",
  "servings": 4,
  "description": "Serves 4-6 people"
}

Time

Timing information. Supports two formats:

Format 1: Numeric values (in minutes)

"time": {
  "prep": 15,
  "active": 30,
  "passive": 60,
  "total": 105
}

Format 2: String values

"time": {
  "prepTime": "15 minutes",
  "cookTime": "45 minutes"
}

Ingredients

Array that can contain strings, ingredient objects, or ingredient subsections.

Simple string:

"ingredients": [
  "2 cups flour",
  "1 cup sugar"
]

Ingredient object:

"ingredients": [
  {
    "id": "flour-1",
    "item": "all-purpose flour",
    "quantity": {
      "amount": 2,
      "unit": "cups"
    },
    "name": "Flour",
    "aisle": "Baking",
    "prep": "sifted",
    "prepAction": "sift",
    "prepTime": 5,
    "destination": "mixing bowl",
    "scaling": {
      "type": "linear",
      "factor": 1.0
    },
    "critical": false,
    "optional": false,
    "notes": "Can substitute bread flour"
  }
]

Ingredient subsection:

"ingredients": [
  {
    "subsection": "For the dough",
    "items": [
      {
        "item": "flour",
        "quantity": { "amount": 2, "unit": "cups" }
      }
    ]
  }
]

Ingredient Scaling

Scaling types:

  • linear — Scales proportionally
  • discrete — Scales in discrete steps
  • proportional — Scales relative to another ingredient
  • fixed — Does not scale
  • bakers_percentage — Baker's percentage (requires referenceId)
"scaling": {
  "type": "bakers_percentage",
  "referenceId": "flour-1",
  "factor": 100,
  "roundTo": 0.25,
  "min": 0.5,
  "max": 10
}

Instructions

Array that can contain strings, instruction objects, or instruction subsections.

Instruction Fields

FieldTypeRequiredDescription
textstringYesHuman-readable step instructions.
imagestringNoURL for a step-specific reference image.
dependsOnstring[]NoUpstream step IDs required before this step begins.
inputsstring[]NoIngredient IDs or equipment IDs consumed in this step.

Simple string:

"instructions": [
  "Preheat oven to 350°F.",
  "Mix dry ingredients."
]

Instruction object:

"instructions": [
  {
    "id": "step-1",
    "text": "Mix dry ingredients in a large bowl.",
    "image": "https://example.com/steps/mix-dry.jpg",
    "destination": "mixing bowl",
    "dependsOn": [],
    "inputs": ["flour-1", "sugar-1"],
    "timing": {
      "duration": 5,
      "type": "active",
      "scaling": "linear"
    }
  }
]

Instruction subsection:

"instructions": [
  {
    "subsection": "Prepare the dough",
    "items": [
      "Mix flour and water",
      {
        "text": "Knead for 10 minutes",
        "timing": {
          "duration": 10,
          "type": "active",
          "scaling": "fixed"
        }
      }
    ]
  }
]

Instruction Timing

  • duration — Time in minutes (number, required)
  • type — "active" or "passive" (string, required)
  • scaling — "linear", "fixed", or "sqrt" (string, optional)

Image Support

Soustack recipes support images at both the recipe level and the instruction level.

Recipe Image

FieldTypeRequiredDescription
imagestring | string[]NoHero image (or set of derivatives) that represents the finished recipe.

Single image:

{
  "image": "https://example.com/recipe.jpg"
}

Multiple images:

{
  "image": [
    "https://example.com/1x1.jpg",
    "https://example.com/16x9.jpg"
  ]
}

Instruction Image

FieldTypeRequiredDescription
imagestringNoReference image that illustrates the instruction.
{
  "instructions": [
    {
      "text": "Brown the beef until caramelized",
      "image": "https://example.com/browning.jpg"
    }
  ]
}

Normalization Rules

  • Accept string, string[], or null/undefined; coerce to string[] internally.
  • Trim whitespace and discard empty strings before persisting.
  • Resolve relative URLs to absolute URLs prior to export.
  • Deduplicate URLs while preserving the first occurrence as the canonical hero image.

Schema.org Conversion

When importing from Schema.org, normalize ImageObject structures to URL strings.

Schema.orgSoustack
"url""url"
["url1", "url2"]["url1", "url2"]
{ "@type": "ImageObject", "url": "..." }"url"
[{ "@type": "ImageObject", "url": "..." }]["url", ...]

Soustack retains the order emitted by Schema.org so responsive clients can pick the most appropriate rendition. Missing url fields should be dropped rather than converted into empty strings.

Image Patterns

Recipes use images in four distinct ways:

PatternRecipe ImageStep ImagesDescription
HeroYesNoSingle finished dish photo.
Step-by-stepNoYesTutorial style with process photos.
HybridYesYesHero image plus inline step images.
NoneNoNoText-only recipe.

Implementations should detect the pattern and render appropriately (see guide for detection logic).

Image Code Examples

Hero pattern (single image)

{
  "soustack": "0.2",
  "name": "Chocolate Cake",
  "image": "https://example.com/cake.jpg",
  "ingredients": ["Flour", "Sugar", "Cocoa powder"],
  "instructions": [
    "Preheat oven to 350°F.",
    "Bake until a skewer comes out clean."
  ]
}

Multiple recipe images

{
  "soustack": "0.2",
  "name": "Summer Salad",
  "image": [
    "https://example.com/salad-1x1.jpg",
    "https://example.com/salad-16x9.jpg"
  ],
  "ingredients": ["Greens", "Tomatoes", "Vinaigrette"],
  "instructions": [
    "Dress the greens right before serving."
  ]
}

Step-by-step pattern

{
  "soustack": "0.2",
  "name": "Fresh Pasta",
  "ingredients": ["Flour", "Eggs"],
  "instructions": [
    {
      "text": "Create a well in the flour",
      "image": "https://example.com/pasta-1.jpg"
    },
    {
      "text": "Add eggs to the well",
      "image": "https://example.com/pasta-2.jpg"
    }
  ]
}

Hybrid pattern

{
  "soustack": "0.2",
  "name": "Sheet-Pan Fajitas",
  "image": "https://example.com/fajitas-hero.jpg",
  "ingredients": ["Peppers", "Onions", "Tortillas"],
  "instructions": [
    {
      "text": "Season the vegetables",
      "image": "https://example.com/fajitas-1.jpg"
    },
    {
      "text": "Broil until charred",
      "image": "https://example.com/fajitas-2.jpg"
    }
  ]
}

Equipment

Array of equipment objects:

"equipment": [
  {
    "id": "oven-1",
    "name": "Oven",
    "required": true,
    "label": "Preheated to 350°F",
    "capacity": {
      "amount": 1,
      "unit": "sheet pan"
    },
    "scalingLimit": 2,
    "alternatives": ["Toaster oven", "Air fryer"]
  }
]

Storage

Storage information for the finished recipe:

"storage": {
  "roomTemp": {
    "duration": "P2D",
    "method": "Airtight container",
    "notes": "Best consumed within 2 days"
  },
  "refrigerated": {
    "duration": "P1W",
    "method": "Covered container"
  },
  "frozen": {
    "duration": "P3M",
    "method": "Freezer bag",
    "thawing": "Thaw overnight in refrigerator"
  },
  "reheating": "Reheat in 350°F oven for 10 minutes",
  "makeAhead": [
    {
      "component": "Dough",
      "storage": "refrigerated",
      "duration": "P2D",
      "method": "Plastic wrap",
      "notes": "Bring to room temperature before using"
    }
  ]
}

Duration uses ISO 8601 duration format (e.g., "P2D" = 2 days, "P1W" = 1 week, "P3M" = 3 months).

Substitutions

Array of substitution objects:

"substitutions": [
  {
    "ingredient": "butter",
    "critical": false,
    "notes": "Butter provides flavor and texture",
    "alternatives": [
      {
        "name": "Margarine",
        "ratio": "1:1",
        "notes": "May affect flavor",
        "impact": "Slight flavor difference",
        "dietary": ["vegan"]
      },
      {
        "name": "Coconut oil",
        "ratio": "1:1",
        "notes": "Use refined for neutral flavor",
        "dietary": ["vegan", "paleo"]
      }
    ]
  }
]

Schema Reference

The complete JSON Schema is available at http://soustack.org/schema/v0.2. Documents should validate against this schema to ensure compatibility.

Encoding

Soustack documents must be valid JSON encoded as UTF-8. The recommended file extension is .soustack.json or .soustack.