# Batch handlers

> Process several jobs of one type through a shared application call while preserving per-job outcomes.

Use TypeScript's `Worker.handleBatch`, Go's `Worker.HandleBatch`, or Python's
`Worker.handle_batch` when one application call can process several jobs more efficiently. Each
member keeps its own durable identity, lease, fence, retry budget, checkpoints, progress, and
cancellation.

<Tabs items={["TypeScript", "Python", "Go"]}>
  <Tab value="TypeScript">
    ```ts
    worker.handleBatch("email.send", { maxSize: 20, lingerMs: 50 }, async (items) => {
      const deliveries = await emailProvider.sendMany(items.map((item) => item.payload));
      return deliveries.map((delivery) =>
        delivery.error
          ? { status: "failed", error: delivery.error }
          : { status: "succeeded", result: { providerId: delivery.id } },
      );
    });
    ```
  </Tab>
  <Tab value="Python">
    ```python
    def send_batch(items: list[BatchHandlerItem]) -> list[BatchSucceeded | BatchFailed]:
        deliveries = email_provider.send_many([item.payload for item in items])
        return [
            {"status": "failed", "error": delivery.error}
            if delivery.error
            else {"status": "succeeded", "result": {"providerId": delivery.id}}
            for delivery in deliveries
        ]

    worker.handle_batch("email.send", send_batch, max_size=20, linger_ms=50)
    ```

  </Tab>
  <Tab value="Go">
    ```go
    worker.HandleBatch("email.send", workhorse.BatchHandlerOptions{
        MaxSize: 20,
        Linger:  50 * time.Millisecond,
    }, func(items []workhorse.BatchHandlerItem) []workhorse.BatchHandlerOutcome {
        outcomes := make([]workhorse.BatchHandlerOutcome, len(items))
        for index := range items {
            outcomes[index] = workhorse.BatchSucceeded{Result: map[string]any{"sent": true}}
        }
        return outcomes
    })
    ```
  </Tab>
</Tabs>

`maxSize` caps the group, while `lingerMs` lets a partial group wait briefly for peers. Claims still
pass through normal priority, queue, concurrency, keyed, and rate-limit admission, so a group can
remain partial until its linger ends.

The callback returns one ordered outcome per member. If it throws or returns an invalid list,
Workhorse submits the failure for every member, then each job applies its own retry policy.

Python passes `max_size` and `linger_ms` as keyword arguments. Its callback returns mappings with
the same `succeeded` and `failed` statuses. Each item carries a `BatchHandlerContext` with
`get_progress` and `set_progress`.

Go passes `MaxSize` and `Linger` through `BatchHandlerOptions`. Its callback returns positional
`BatchSucceeded` or `BatchFailed` values, and each item carries a `BatchHandlerContext` with its
standard cancellation context, checkpoint operation, `GetProgress`, and `SetProgress`.

Workhorse records each shared invocation before the callback starts. The task drawer shows the
batch size and links the other members. If the shared callback fails, the drawer identifies that
failure across the group instead of guessing from the individual task errors.

Batch contexts omit timers, signals, human waits, and child joins because one member cannot suspend
and replay independently inside a shared invocation. Register an ordinary handler when a job needs
those boundaries.

TypeScript configures grouping with `BatchHandlerOptions.maxSize` and
`BatchHandlerOptions.lingerMs`; each option changes assembly of the batch without changing an
individual job's lifecycle.

## Next

- [Workers](/docs/workers) — size the capacity that batch members occupy
- [Priority](/docs/priority) — order members before grouping
- [Retries](/docs/retries) — apply failures per member

---

Exact batch limits and lifecycle rules:
[architecture reference](https://github.com/stablemates/workhorse/blob/main/docs/architecture.md#worker-concurrency-and-lifecycle).
