Every structured-output library's README shows you the same demo: extract a name and age from a sentence, get back clean JSON. Cool, but that's not really a use case, that's a unit test.
Here's what I've actually reached for shapecraft for, across a handful of real projects. Five short examples, no fluff, so you can see where this fits before you commit to reading the full docs.
1. Turning support tickets into triageable data
A raw customer message ("hey my invoice from last month looks wrong and also the app crashed twice") isn't something you can route or prioritize as-is. Give it a schema instead:
import { generate, openai } from "@aviasole/shapecraft";
import { z } from "zod";
const TicketSchema = z.object({
category: z.enum(["billing", "bug", "how-to", "account"]),
priority: z.enum(["low", "normal", "urgent"]),
summary: z.string(),
});
const result = await generate(openai({ model: "gpt-4o-mini" }), TicketSchema, rawMessage);
One call, one typed object, ready to drop into a queue.
2. Parsing receipts, invoices, and other "almost structured" documents
OCR'd text from a receipt is a mess of line items, totals, and stray whitespace. Instead of writing regex to hunt for the total, describe the shape you want and let the model do the extraction:
const ReceiptSchema = z.object({
vendor: z.string(),
total: z.number(),
lineItems: z.array(z.object({ name: z.string(), price: z.number() })),
});
Same generate() call, same guarantees, just a different schema.
3. Slot-filling for a form-driven chatbot
Onboarding flows ("what's your name, email, and company size?") don't need a whole conversation engine, they need a bot that keeps asking until it has every required field, validated. shapecraft's generate() retry loop already does the "keep trying until it's valid" part, so the bot logic is just: ask, extract, check what's missing, repeat.
4. Running a local model with a real guarantee
Not everything can leave the device. For document classification that has to run fully offline, llamaCpp() applies a GBNF grammar at the token level, so the model cannot produce anything outside your five category labels, not "usually doesn't," actually can't:
import { llamaCpp } from "@aviasole/shapecraft";
const model = llamaCpp({ modelPath: "./models/llama-3.2-3b.gguf" });
const result = await generate(model, { gbnf: categoryGrammar }, chunk);
That's the difference between constrained and best-effort guarantee levels, and it matters a lot more once you're running unattended over a few thousand files overnight.
5. Batch-processing a folder of anything
Classify, summarize, or extract from a directory of files without hand-rolling concurrency control:
import { generateBatch } from "@aviasole/shapecraft";
const results = await generateBatch(
model,
schema,
files.map((f) => f.text),
{ concurrency: 5 }
);
Each item validates and retries independently, so one bad file doesn't take down the batch.
That's the range
Ticket triage, document extraction, chatbot slot-filling, offline classification, batch jobs, all the same core idea: describe the shape you want, get back something you can actually trust. Check result.guaranteeLevel any time you want to know exactly how much.
One more thing: we've now published full documentation, with a guide for every feature mentioned here (and a bunch not covered in this post, like streaming, tool calling, and multi-agent orchestration). Go explore it at aviasoletechnologies.github.io/shapecraft.
Top comments (4)
the guaranteeLevel distinction in #4 is the most useful part of this for me, most structured-output libs pretend cloud JSON mode and grammar-constrained local decoding are the same guarantee when they're really not. for the batch case in #5, when one file in generateBatch exhausts its retries and never validates, does that item resolve to null/undefined in the results array or does the whole batch reject, curious how you're surfacing partial failures in an overnight run where nobody's watching
It's actually based on Promise.allSettled. Each item resolves independently, returning either { status: "fulfilled", value } or { status: "rejected", reason }. When you run this overnight, you can simply check the results the next morning for any with status === "rejected." The reason will typically be the specific error, like MaxRetriesExceededError. The rest of the batch remains unaffected. So, if one file fails, it doesn’t derail your entire run.
Makes sense, allSettled is the right primitive for a run you are not babysitting. The bit I would want on top is a clean way to re-run just the rejected subset the next morning without touching the fulfilled ones. Is that built in or do you script it off the results array yourself?
Good question. That isn’t built in as a separate helper right now, but it’s easy to do because results preserve the original order:
Fulfilled items stay untouched, and only the rejected subset runs again.