close
Skip to content
Batchwork
Esc
navigateopen⌘Jpreview
Latest update — v1.2.1 released

Save up to 50% on LLMOpenAIAnthropicGoogle GeminiMistral AIX.aiGroqTogether AI 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.

Read the docs
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 more
import { 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 more
await 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 more
import { 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());