# Forecasts

> Snapshot the deals forecast for a period, keep the deals behind each number, and compare two snapshots deal by deal.

Source: https://bettersupabase.com/docs/blocks/forecasts

The `forecasts` block freezes the [deals](/docs/blocks/deals) forecast call
at a point in time. A snapshot stores the totals for a period and currency
(won, commit, best case, pipeline, not called, omitted, later, attainment and
the gap to the target) and a copy of every deal behind them, so next week's
review can show what moved: which deals were won, lost, added or changed.

```bash
better-supabase add forecasts   # adds tenant, access and deals as well
```

| Permission           | Lets a member                   | Default roles           |
| -------------------- | ------------------------------- | ----------------------- |
| `forecasts.read`     | list and open snapshots         | `member`, `admin`       |
| `forecasts.snapshot` | take a snapshot of the forecast | `admin` (`forecasts.*`) |

The service role can always take a snapshot, so a weekly cron job works
without a member session.

## Take a snapshot [#take-a-snapshot]

```ts
import {
  createForecasts,
  rpcTransport,
} from "better-supabase/blocks/forecasts";
import { money } from "better-supabase/commerce";

const forecasts = createForecasts({ transport: rpcTransport(supabase) });

const snapshot = await forecasts
  .snapshot(organizationId, {
    since: Temporal.PlainDate.from("2026-10-01"),
    until: Temporal.PlainDate.from("2026-12-31"),
    currency: "EUR",
    target: money(25_000_000n, "EUR"),
  })
  .orThrow();

snapshot.totals.commit; // a Money value, as deals.forecast returns it
snapshot.dealCount; // the deals copied into the snapshot
```

A snapshot sums every deal of the organization in the currency, whoever owns
it, so the numbers match for everyone who opens it later. It copies the open
deals that close in the period and the deals won or lost in it. Pass a
`pipelineId` to snapshot one pipeline. A period runs at most 366 days.

## Compare two snapshots [#compare-two-snapshots]

```ts
const [latest, previous] = await forecasts
  .list(organizationId, { limit: 2 })
  .orThrow();

const { deals } = await forecasts.changes(previous.id, latest.id).orThrow();

for (const deal of deals) {
  deal.change; // "added", "removed", "won", "lost" or "changed"
  deal.before?.amount;
  deal.after?.amount;
}
```

`list` returns snapshots newest first and filters by `pipelineId`, `since`,
`until` and `limit`. `get(snapshotId)` returns one snapshot with its deals.
`changes` lists only the deals whose stage, owner, forecast category,
outcome, amount or close date differ between the two snapshots, and needs
two snapshots of the same organization and currency.

## Events [#events]

Every snapshot writes `forecast.snapshotted` to the
[outbox](/docs/blocks/outbox) with the snapshot id, the pipeline, the period,
the currency and the deal count. See [Events](/docs/extending/events).

| Error code                  | When                                                        |
| --------------------------- | ----------------------------------------------------------- |
| `FORECAST_FORBIDDEN`        | the caller lacks `forecasts.snapshot`                       |
| `FORECAST_INVALID`          | a bad period, currency or target, or a period over 366 days |
| `FORECAST_PIPELINE_UNKNOWN` | the pipeline is not one of the organization's               |
| `FORECAST_NOT_FOUND`        | no snapshot you may read has the id                         |
| `FORECAST_MISMATCH`         | the two snapshots differ in organization or currency        |