Save up to 50% on LLM





costs
Unified batch API for AI providers. Process LLM requests in bulk with a single call for lower costs. Processing, uploading, polling, and result parsing handled for you.
import { batch } from "batchwork";
import { openai } from "@ai-sdk/openai";
const job = await batch({
model: openai.chat("gpt-5.5"),
requests: docs.map((doc) => ({
customId: doc.id,
prompt: `Summarize: ${doc.text}`,
})),
});
for await (const result of job.results()) {
await saveSummary(result.customId, result.text);
}One workflow for every batch API.
Keep each provider's native batch endpoint underneath, while your app gets a stable contract for jobs, status, and results. Change models without rebuilding the pipeline around them.
Native APIs stay native
Keep provider-native batch endpoints and pricing underneath, with one application contract above them.
One job lifecycle
Track submission, status, cancellation, rehydration, and delivery through the same lifecycle everywhere.
Consistent results
Handle unordered output, failures, usage, and raw responses through a normalized result type keyed by customId.
One call replaces the provider plumbing
Keep the request shape you already use with the AI SDK. Batchwork serializes the right provider body, submits the batch, tracks the lifecycle, and gives you one job handle back.
Learn moreimport { batch } from "batchwork";
const job = await batch({
model: "anthropic/claude-opus-4-8",
requests: docs.map((doc) => ({
customId: doc.id,
prompt: `Summarize: ${doc.text}`,
maxOutputTokens: 512,
})),
});
console.log(job.id, job.provider, job.status);Await and process results anywhere
When the provider finishes, read normalized results from any script, worker, queue, or backend. Every result is correlated by customId, with one status and usage shape across providers.
Learn moreawait job.wait();
for await (const result of job.results()) {
if (result.status !== "succeeded") {
await retrySummary(result.customId, result.error);
continue;
}
await saveSummary(result.customId, {
text: result.text,
usage: result.usage,
});
}No long-running wait loops in production
For production, register each job once and let Batchwork deliver a signed event when it finishes. Native OpenAI webhooks and managed polling collapse into the same delivery path.
Learn moreimport { batch } from "batchwork";
import { createBatchPoller, createMemoryStore } from "batchwork/server";
const poller = createBatchPoller({
store: createMemoryStore()
});
const job = await batch({ model, requests });
await poller.track(job, {
webhookUrl: "https://acme.com/webhooks/batch",
secret: process.env.BATCH_WEBHOOK_SECRET,
});
// Run on a schedule. OpenAI can use native webhooks;
// every other provider is polled behind the same API.
export const GET = async () => Response.json(await poller.tick());Keep the core small, then add the pieces your app needs.
Use the plain job handle for scripts and workers, then layer in route handlers, durable stores, and provider coverage as your workload moves into production.
Route handlers without the boilerplate
Export App Router GET and POST handlers for Cron ticks and native webhooks, while onComplete runs in-process so results can go straight to your database.
Learn moreResume work by batch id
Persist the provider batch id and reconnect later from another process. No original request payloads required.
Learn moreSeven providers, one result model
OpenAI, Anthropic, Gemini, Groq, Mistral, Together AI, and xAI all return one normalized BatchResult shape.
Learn more