URL: /capabilities/measure/weekly-readout · Id: `page:/capabilities/measure/weekly-readout`

# Weekly readout

> Each week, each running Play’s numbers beside its benchmark, then keep, fix or switch, naming the row that fired.

Each week, each running [Play](https://startupbuilder.dev/business/nouns/play)’s numbers beside its benchmark, then keep, fix or switch, naming the row that fired.

```text title="Run it · Ask"
Read this week’s numbers for every running Play and tell me what to keep, fix or switch. Spend nothing.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Play.review({ goal: 'Read this week’s numbers for every running Play and tell me what to keep, fix or switch. Spend nothing.', budget: { amount: 0, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "Free",
  "sample": {
    "play": "Cold email",
    "metric": "reply rate",
    "week": "1.6% of 250 (0.7% to 3.5%)",
    "benchmark": "3% to 6%",
    "verdict": "fix",
    "fired": "replies < 2% at 200 sends: angle, not volume"
  }
}
```

- **Kind:** Workflow
- **Price:** Free
- **Your time:** 7 minutes
- **Approval:** Runs on its own
- **Deepens:** [`workflow:weekly-review`](https://startupbuilder.dev/workflows/processes/iteration/weekly-review)

## What you get
- **1 week** · Each week, each running Play’s numbers beside its benchmark, then keep, fix or switch, naming the row that fired. · [`noun:play`](https://startupbuilder.dev/business/nouns/play)
- `plays[].numbers` · The week’s numbers for the Play’s Metric and its funnel rows, each beside its benchmark and its source label.
- `plays[].interval` · The 90% interval on every rate: 2 replies from 60 sends reads “3.3%, 1.1% to 9.6%”, so one reply never decides a switch.
- `plays[].verdict` · `keep` (every `good` row for this week holds), `fix` (a `breaks` row fired and its fix is a change inside the Play) or `switch` (two `breaks` rows fired two weeks running).
- `plays[].fired` · The typed row that fired: its metric, the threshold, this week’s reading, the cause and the call that fixes it.

**How long:** Seconds.

## Trigger
It runs weekly at 08:00 on the first weekday, after the week’s Search readout lands.

```ts
every('mon 08:00', ($) => $.Play.review({ plays: 'running' }))
```

## Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.

| Step | Verb | Returns | Price | Approval |
| --- | --- | --- | --- | --- |
| 1. Read each running Play’s numbers for the week | [`Metric.query`](/code/apps/analytics-and-experiments/query) | The Metric and the funnel rows for each Play, with n | Free | Runs on its own |
| 2. Read the benchmarks and the typed rows | `Collection.query` | Each Play’s `good` row for this week and its `breaks` rows, with their thresholds and source labels | Free | Runs on its own |
| 3. Hold each reading against its rows | `Metric.watch` | Which `breaks` rows fired, which `good` rows held, and the 90% interval on every rate | Free | Runs on its own |
| 4. Write the verdict by the declared rule | `Collection.query` | Keep, fix or switch per Play, with the row that fired and the call that fixes it | Free | Runs on its own |

Calls: `Metric.query`, then `Collection.query`, then `Metric.watch`, then `Collection.query`.

Free, each time it runs, capped at 0¢.

## Example: give it a goal
> Read this week’s numbers for every running Play and tell me what to keep, fix or switch. Spend nothing.

The plan it shows runs 4 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| A Play’s Metric has no reading this week | The Play reads `no-data` with the Event it waits on, never a zero; a missing feed is a fix, not a switch. |
| n is under `minN` | The verdict can be `keep` or `fix`, never `switch`: at 60 sends a 3.3% reply rate could be anywhere from 1.1% to 9.6%. |
| Two `breaks` rows fire for one Play | The verdict names both, the first by the order its page lists them, and the switch waits one more week unless both fired last week too. |

Each step retries three times, then falls through to the next route; one item's failure lands in `skipped` with its reason, never failing the call. Before a step would cross `maxCost` it stops and returns what it has, with a `resume` link. Each step's idempotency key is the call's key plus the step and the item: `leaseline-play-review-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Wait two weeks before any switch. | [`Play.review`](/capabilities/measure/weekly-readout) |
| Apply the fix the readout names for Cold email. | `Play.apply` |
| Rebuild next week’s mix from this readout. | `PlayMix.compile` |

```ts
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'weekly-review',
  verb: 'Play.review',
  input: { plays: "'running' | string[]", minN: 'integer' },
  trigger: { schedule: 'mon 08:00' },
  maxCost: 0,
  execute: async (ctx, { plays = 'running', minN = 100 }) => {
    const numbers = await ctx.step('numbers', () => $.Metric.query({ plays, period: 'week', withN: true }))
    const rows = await ctx.step('rows', () => $.Collection.query({ from: 'play', where: plays === 'running' ? "state = 'running'" : `$id in ${plays}`, fields: ['breaks', 'good'] }))
    const held = await ctx.step('watch', () => $.Metric.watch({ readings: numbers, against: rows, interval: 0.9, minN }))
    return verdicts(held)        // keep: every good row holds · fix: a breaks row fired · switch: two rows, two weeks running
  },
})
```

Extend it with `extends: 'Play.review'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [Build in public](/plays/build-in-public), [Commission partners](/plays/commission-partners), [Creator partnership](/plays/creator-partnership), [Creator retainers](/plays/creator-retainers), [Made-with line](/plays/made-with), [Sponsorships](/plays/sponsorships).

It serves [Measure](/capabilities/measure), and runs in [Hands-on](/strategy/hands-on), [Paid](/strategy/paid), [Borrowed](/strategy/borrowed), [Keep and grow](/strategy/keep-and-grow). It deepens [`workflow:weekly-review`](https://startupbuilder.dev/workflows/processes/iteration/weekly-review).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Play.review` |
| Guide approval | `none` |
| Goal | Read this week’s numbers for every running Play and tell me what to keep, fix or switch. Spend nothing. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | `every('mon 08:00', ($) => $.Play.review({ plays: 'running' }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-play-review-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Read each running Play’s numbers for the week | None | None. |
| 2. Read the benchmarks and the typed rows | None | None. |
| 3. Hold each reading against its rows | None | None. |
| 4. Write the verdict by the declared rule | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Read each running Play’s numbers for the week | `code` | Code | Code checks each result against its declared type | `measured` | `$0.plays, $0.week` |
| 2. Read the benchmarks and the typed rows | `code` | Code | Code checks each result against its declared type | `measured` | `$0.plays` |
| 3. Hold each reading against its rows | `code` | Code | Code checks each result against its declared type | `measured` | `$1, $2, $0.minN` |
| 4. Write the verdict by the declared rule | `code` | Code | Code checks each result against its declared type | `measured` | `$3` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | [Analytics and experiments](/code/apps/analytics-and-experiments) |
| Nouns | [`noun:metric`](https://startupbuilder.dev/business/nouns/metric), [`noun:collection`](https://startupbuilder.dev/business/nouns/collection) |
| Events | `Metric.queried`, `Collection.queried`, `Metric.watched` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `plays` | `'running' \| PlayId[]` | `'running'` | The Plays to read: every Play running this week, or the ones you name. |
| `week` | `string` | `the last full ISO week` | The week read. |
| `minN` | `integer` | `100` | Below this many sends, visits or clicks, the verdict can fix but never switch: the interval is too wide to call. |
| `maxCost` | `number` | `0.00` | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a `resume` link. |

Emits `Play.reviewed`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).