DEV Community

Poojan
Poojan

Posted on • Originally published at poojan.technokari.com on

I Automated 90% of a Grungy Data-Entry Job Without an LLM — and Where an Agent Would Actually Earn Its Place

TL;DR — Onboarding a restaurant meant hand-entering hundreds of menu items and finding an image for each — hours of mind-numbing work per client. The 2026 reflex is "point an AI agent at it." I didn't. I built a deterministic pipeline — bulk import, normalize, fuzzy-match images by filename, auto-apply the confident matches — that cleared ~90% of the work with code that is cheap, debuggable, and correct-by-construction. The remaining ~10% (genuinely ambiguous matches) goes to a human-in-the-loop review queue. Then I mark the two specific spots where a vision-language model does pay for itself. The lesson isn't "AI bad" — it's deterministic-first, AI where it's uniquely better, and never an agent for what a for loop does more reliably.

The grungy job

Every new restaurant on the platform arrives the same way: a spreadsheet (or a PDF, or a photo of a printed menu) with a few hundred items, and a folder of food photos with filenames like paneer_tikka_final_v2.jpg. Someone has to create each item, set its price and category, and attach the right image. It's hours of tedious, error-prone clicking per client, and it's the least fun part of onboarding.

This is exactly the kind of task the current discourse says to hand to an autonomous agent: "let the AI read the menu, find the images, and fill in the system." And you can build that. But before reaching for the most powerful, most expensive, least predictable tool available, it's worth asking what the job actually decomposes into. Most "AI-shaped" grunt work is 90% deterministic plumbing wearing a 10% judgment hat.

Step 1: bulk import beats a smart importer

The first instinct people automate away is data entry itself. But most of the input is already structured — a spreadsheet is a table. It doesn't need intelligence; it needs a parser and validation.

// Parse the sheet, normalize, validate. No model. This is 60% of the "AI" job.
function importMenu(rows) {
  return rows.map((row, i) => {
    const item = {
      name: normalizeName(row.name), // trim, collapse spaces, title-case
      price: parsePrice(row.price), // "₹120" | "120.00" | "120" → 12000 (minor units)
      category: canonicalCategory(row.category), // map free-text → known category set
      external_key: slugify(row.name), // stable key for matching (step 2)
    };
    const problems = validate(item); // missing price? unknown category?
    return { item, rowNumber: i + 2, problems }; // +2: header + 1-index, for human-readable errors
  });
}

Enter fullscreen mode Exit fullscreen mode

Everything a model would "understand" here — price formats, category names, whitespace — is a finite, enumerable set of cases. Enumerate them. The payoff: this code is deterministic and testable. The same sheet produces the same output every run, a malformed price is a specific error on a specific row, and there's no token bill. An LLM doing this would be slower, non-reproducible, occasionally hallucinate a category, and cost money per run to do worse than a regex. Wrong tool.

Step 2: match images by filename — fuzzy, but still deterministic

Attaching the right photo sounds like it needs vision. Usually it doesn't, because the filenames already encode the answer: paneer_tikka_final_v2.jpg is obviously the Paneer Tikka. This is a string-matching problem, not a seeing problem.

// Score each image filename against each item name; auto-apply confident matches.
function matchImages(items, filenames) {
  const results = [];
  for (const item of items) {
    const scored = filenames
      .map(f => ({ file: f, score: similarity(slugify(item.name), slugify(stripExt(f))) }))
      .sort((a, b) => b.score - a.score);

    const best = scored[0];
    const gap = best.score - (scored[1]?.score ?? 0); // how clear was the winner?

    if (best.score >= 0.85 && gap >= 0.15) {
      results.push({ item, file: best.file, decision: 'auto' }); // confident → apply
    } else {
      results.push({ item, candidates: scored.slice(0, 4), decision: 'review' }); // ambiguous → human
    }
  }
  return results;
}

Enter fullscreen mode Exit fullscreen mode

The important design choice is the second threshold : not just "is the best match good?" but "is it clearly better than the runner-up?" A high score with a tiny gap ("Chicken Biryani" vs "Chicken Biryani Special", both 0.9) is ambiguous, and the right move is to not guess. That gap check is what makes auto-apply safe — it only fires when the answer is genuinely unambiguous, and everything else falls through to a human instead of being confidently wrong.

Where filenames are missing or useless (IMG_2231.jpg), a web image search by item name fills the gap — fetch a few candidates, present them, still let a human confirm. Deterministic retrieval, human judgment on the final pick.

Step 3: a human-in-the-loop queue for the ambiguous 10%

Here's the part the "full autonomy" pitch gets wrong: the last 10% isn't a failure of automation, it's where the actual judgment lives, and a person resolves it faster and more accurately than a model guessing. So the pipeline's job is to shrink the human's work to only the decisions that need a human — not to eliminate the human.

  spreadsheet ─▶ [import + validate] ─▶ [fuzzy match] ─┬─ auto (90%) ─▶ applied
                                                            └─ review (10%) ─▶ [human queue]
                                                                                     │
                                                                            pick candidate ─▶ applied

Enter fullscreen mode Exit fullscreen mode

The review queue shows the item, the top 3–4 candidate images, and the validation problems — one screen, a few clicks each. A job that was "enter 300 items by hand" becomes "confirm 30 ambiguous matches." That's the win, and it came from ordinary code plus a good UI, not from a model.

Two properties make this trustworthy, and they're the same ones I lean on everywhere: the pipeline is idempotent (re-running on the same input doesn't create duplicates — it matches on external_key, the same discipline behind idempotent order creation), and every auto-decision is logged with its score and gap so a wrong auto-match is auditable and the threshold is tunable from real data.

So where does an LLM actually earn its place?

I'm not arguing against AI here — I'm arguing against using the most expensive, least predictable tool for the parts a for loop does better. There are exactly two spots in this pipeline where a model is genuinely, uniquely better, and it's worth being precise about them:

  1. Unstructured input the parser can't touch. When the menu arrives as a photo of a printed board or a messy PDF, there's no spreadsheet to parse — extracting "item, price, category" from an image is real multimodal understanding, and a vision-language model does it well. This is the honest home for AI in this workflow: turning genuinely unstructured input into the structured rows my deterministic pipeline then takes over. The model does the perception; the boring code does the logic.

  2. Semantic image disambiguation on the hard matches. For the ambiguous 10%, a vision model could look at the candidate photos and the item name and rank them better than filename similarity — "which of these three images is actually Paneer Tikka." That genuinely beats string matching on the residual. But note: it's operating on the pre-filtered hard cases, as an assist to the human reviewer, not as an autonomous decider on all 300 items.

The pattern I'd stand behind: let a model do perception and semantics on the inputs that are truly unstructured or truly ambiguous, and let deterministic code own everything with a definite right answer. That's the opposite of "an agent does the whole job." It's a model as a component with a narrow, well-defined contract — the same way you'd add any other dependency.

A note on cost and reliability, since it's the part the demos skip: running a model over all 300 items, every re-import, is real latency and a real per-run bill for work a regex does for free and a hash does more reliably. Scoped to just the unstructured extraction and the ambiguous residual, the model runs on a tiny fraction of the volume — which is the difference between "AI feature that pays for itself" and "AI tax on a solved problem."

What I'd build next (and what I still wouldn't)

  • Would build: the vision-model extraction path for photo/PDF menus, feeding the same deterministic pipeline. That's a clean, high-value addition with a narrow contract — extract rows, hand off to code.
  • Would build: learn the thresholds from the human queue. Every time a reviewer overrides an auto-match or confirms an ambiguous one, that's labeled data to tune the 0.85/0.15 cutoffs — no model required, just feedback.
  • Still wouldn't build: a fully autonomous "agent" that owns the whole flow end-to-end. It would be less reliable, harder to debug, more expensive, and non-reproducible — trading a pipeline I can reason about for a black box I'd have to babysit. The 10% that needs judgment is cheaper and safer with a human than with an agent pretending to be one.

The one idea to take away

Most "AI could do this" grunt work is a deterministic pipeline with a small core of real judgment. Automate the deterministic 90% with boring, testable, idempotent code; route the ambiguous 10% to a human; and spend an LLM only where it's uniquely better — perception on unstructured input, semantics on the genuinely hard residual. That's not AI-skepticism. It's using the right tool for each part of the job — which is the whole skill.

Top comments (0)