# Meal Flyer

> Use when processing a grocery store flyer for the Meal OS system. Triggered by "/meal-flyer" or "parse a flyer" or "process flyer".

- Skill: `scurry/meal-flyer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add scurry/meal-flyer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/scurry/meal-flyer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: scurry (https://skillmd.com/u/scurry)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/scurry/meal-flyer

---


# /meal-flyer — Store Flyer Parser

Parse a weekly grocery store flyer and extract sale items mapped to canonical ingredient IDs.

**Announce at start:** "Processing store flyer..."

## Step 1: Get Flyer Input

The user provides flyer data in one of two ways:

### Option A: PDF File
If the user provides a file path ending in `.pdf`:
- Read the PDF using the Read tool
- For PDFs over 20 pages, paginate: read pages 1-20, then 21-40, etc.
- Focus on food items — skip pages that are clearly non-food (cleaning supplies, pharmacy, seasonal items)
- After the flyer markdown is successfully written and verified, delete the source PDF so raw flyer files do not accumulate in the repo

### Option B: Pasted Text
If the user pastes text from a store website or flyer:
- Use it directly

If no input is provided, ask: "Please provide the store flyer — either paste the text or give me the path to a PDF file."

## Step 2: Load Config and Identify the Store

Read `data/config.yaml` to get the user's store list.

Determine which store this flyer is from:
- Look for store name in the flyer content
- If not obvious, ask: "Which store is this flyer from?" and list the stores from config as options

## Step 3: Determine Date Range

Look for a validity date range in the flyer content (e.g., "Valid 3/22 - 3/28", "Weekly Specials March 22-28").
- If found, use the **start date** for the filename
- If not found, use today's date
- Record the full date range for frontmatter

## Step 4: Load Canonical Ingredients

Read `data/canonical-ingredients.yaml` from the project root. This is the normalization reference for mapping sale items.

**If the file does not exist:** Stop and tell the user:
"data/canonical-ingredients.yaml is missing. Run /meal-setup first to initialize the project, then re-run /meal-flyer."

## Step 5: Extract Sale Items

For each sale item in the flyer, extract:

| Field | What to capture | Examples |
|-------|----------------|---------|
| Item description | As printed in the flyer | "Boneless Skinless Chicken Thighs Family Pack" |
| Price | The sale price | $2.49, $8.99 |
| Unit | Price unit | /lb, /each, /pint, /dozen, /bag |
| Deal type | Type of promotion | Regular Sale, BOGO, Manager's Special, 2 for $5 |

### Price normalization rules:
- "2 for $5" → price: $2.50, unit: /each, deal type: "2 for $5"
- "Buy One Get One Free" / "BOGO" → price: [listed price], deal type: "BOGO (effective 50% off)"
- "Save $2" → note the savings but use the final sale price if shown
- "$X.XX/lb" → price: $X.XX, unit: /lb
- If price per unit can't be determined, record as-is with the deal description

### Filtering rules:
- **Include:** All food and beverage items
- **Skip entirely:** Non-food items (cleaning supplies, paper goods, pet food, pharmacy, health/beauty). Do not include them in the Sale Items table. Track the count for the confirmation summary only.

## Step 6: Map to Canonical Ingredients

For each food item:

1. **Match against canonical-ingredients.yaml** — find the closest canonical ID:
   - "Boneless Skinless Chicken Thighs Family Pack" → `chicken_thigh`
   - "Fresh Atlantic Salmon Fillets" → `salmon`
   - "Grape Tomatoes Pint" → `cherry_tomato`
   - "Greek Yogurt Assorted" → `greek_yogurt`

2. **Unmappable food items** — if a food item doesn't match any canonical ingredient:
   - Mark with `?` in the canonical ID column
   - These go into an "Items Needing Review" section for the user to decide if they should be added to the canonical list

3. **Non-food items** — already excluded in Step 5; do not include any row for them in the table

## Step 7: Generate Summary

Identify which ingredients from the core set (~24 items in canonical-ingredients.yaml) are on sale this week. List them with price and store name.

If no core ingredients are on sale, note: "No core ingredients on sale this week."

## Step 8: Detect Likely Loss Leaders

Identify items priced aggressively low — likely below store cost to drive foot traffic.

### Method 1 — Savings Heuristic (always available)

For each sale item with a "Save $X" value: compute `savings_pct = save_amount / (sale_price + save_amount)`. Flag as loss leader if `savings_pct >= 0.35`.

### Method 2 — Price History Comparison (when data exists)

Read `data/receipts/price-history.yaml` if it exists.

For each sale item that has a canonical ID (not `?`):
1. Look up the canonical ID in the price history
2. Compute the average price from all ledger entries for that item
3. If the sale price is >30% below the average, flag as a likely loss leader

If the price history file doesn't exist, skip Method 2 silently.

### Output

If any items are flagged, add a new section to the flyer markdown between "Core Ingredients On Sale This Week" and "Items Needing Review":

````markdown
## Likely Loss Leaders

| Item | Sale Price | Regular/Avg Price | Savings | Method |
|------|-----------|-------------------|---------|--------|
| [Item] | $X.XX/unit | $Y.YY (regular) | NN% | heuristic |
| [Item] | $X.XX/unit | $Y.YY (avg from N receipts) | NN% | price-history |
| [Item] | $X.XX/unit | $Y.YY (regular) / $Z.ZZ (avg) | NN% | both |

Consider building meals around these items — they're priced to get you in the store.
````

If an item is flagged by both methods, show it once with Method = "both" and show both the regular and average prices.

If no items meet either threshold, omit this section entirely.

## Step 9: Check for Collisions

Generate filename: `data/flyers/YYYY-MM-DD-<store-slug>.md`
- Store slug: `market-basket` or `hannafords`
- Example: `data/flyers/2026-03-22-market-basket.md`

If the file already exists, ask: "A flyer for [store] on [date] already exists. Overwrite, save as [date]-[store]-2.md, or cancel?"

## Step 10: Write the Flyer File

Ensure the directory exists: run `mkdir -p data/flyers` via Bash before writing.

Write to `data/flyers/YYYY-MM-DD-<store-slug>.md` using this format:

```markdown
---
store: [Store Name]
date_range: [YYYY-MM-DD to YYYY-MM-DD]
source: [weekly-flyer-pdf or pasted-text]
---

## Sale Items

| Item | Canonical ID | Price | Unit | Deal Type |
|------|-------------|-------|------|-----------|
| [Item description] | [canonical_id or ?] | [$X.XX] | [/unit] | [deal type] |

## Core Ingredients On Sale This Week

- [canonical_id] — $[price]/[unit] ([Store Name])
- [canonical_id] — $[price]/[unit] ([Store Name])

<!-- Include this section only if Step 8 flagged any loss leaders; omit entirely if none -->
## Likely Loss Leaders

| Item | Sale Price | Regular/Avg Price | Savings | Method |
|------|-----------|-------------------|---------|--------|
| [Item] | $X.XX/unit | $Y.YY (regular) | NN% | heuristic |

Consider building meals around these items — they're priced to get you in the store.

## Items Needing Review

- [Item description] → marked `?` ([reason or suggestion for canonical mapping])
```

If the source input was a local PDF file and the markdown flyer was written successfully, delete the PDF after saving the markdown output. Do not keep a separate archived PDF copy.

## Step 11: Confirm

Output a summary:

```
Flyer saved: data/flyers/YYYY-MM-DD-<store-slug>.md

  Store: [Store Name]
  Date range: [date range]
  Items extracted: [N total]
  Mapped to canonical: [M items]
  Marked for review (?): [K items]
  Skipped (non-food): [J items]

  Core ingredients on sale:
  - [list or "none"]

  Loss leaders detected: [N items, or "none"]
  - [list if any]

  Source PDF deleted: [yes/no/not-applicable]

Review items marked with ? and decide if they should be added to data/canonical-ingredients.yaml.
```

