# Build a scheduled report

Use when someone wants a report written for them again and again, on a schedule or when something happens ("every morning, report on my campaigns against yesterday"): CoDNA reads the data, asks a model to write the page, and the people who should read it find it in CoDNA.

It covers finding the data, writing the script, agreeing the report from a real preview, who reads it, and switching it on. In CoDNA's web UI the result is an **agent**: an automation that asks a model.

## First, is this the right tool?

1. **Once, not for good: write it yourself.** Read the data, write the page in `output`. An agent is for a report that keeps coming.
2. **Numbers or a list, no prose: leave the model out.** A script can build the table, or list each page with its own first line (`codna.find` with `includeContent`), and write the page with `actions.write`. It costs nothing, works while asking is switched off, and never words anything wrong.
3. **Check `list_automations`** for an agent that already writes it, and change that one with `update_automation`.
4. **Check `list_models`.** If asking a model is switched off, or no budget is set, every ask is refused. Whoever manages the instance switches it on and sets the month's budget: in the console's Models tab for an instance CoDNA hosts, or on its own host with `codna model` and `codna automations switch model on`. Say so, and build the agent meanwhile.

## 1. Find the data

- **In CoDNA itself,** such as the week's new decisions: the script reads the pages with `codna.find` and `codna.get`, as its owner. `pathPrefix` names a folder and everything beneath it. `updatedAfter` takes a time. Each page found has `address`, `title`, `createdAt` and `updatedAt` (and `content`, with `includeContent`). A page's `createdBy` is an id: a report cannot name who wrote it yet. A run reads CoDNA at most 20 times.
- **Outside CoDNA:** `list_connections` lists the sources the person may read, kind `http` (a JSON API) or `postgres` (a query), each with what it is for. Pick the one that holds the numbers. How a script reads one (an `inputs` export, and `source()`) is in `list_connections`' description, not `create_automation`'s. At most 10 reads a run, 5 MB and 10,000 rows each.
- **The source is missing?** Say which data, and where it lives, and send the person to the Connections page that `list_connections` names. They paste its address and token there. Never ask for a token in the chat, and never put one in a script or a page.
- **The period is the tick's,** not a date worked out from the time. A scheduled run has `event.previous` and `event.due`: when the tick before it was due, and when it was due, in UTC. Report on what happened from `previous` until `due`. That is the person's whole day in their time zone, Friday to Monday on a weekday schedule, and nothing is counted twice. Scripts have no `Date`.

## 2. Write the script, and register it switched off

A report from a source, the period's numbers:

```js
export function inputs({ event }) {
  return { stats: source('ads', { path: '/v1/stats', query: { from: event.previous, to: event.due } }) }
}

export default function ({ event, inputs, actions }) {
  const day = event.due.slice(0, 10)
  if (inputs.stats.error) {
    actions.notify('me', 'The campaign report could not read the numbers: ' + inputs.stats.error)
    return
  }
  actions.ask('Write the campaign report as the template says, for ' + event.previous + ' to ' + event.due + ':\n\n' +
      JSON.stringify(inputs.stats.data),
    { documents: ['wiki:templates/daily-campaign-report'], into: 'output:reports/campaigns/' + day, title: 'Campaigns, ' + day })
}
```

With a `postgres` source the period goes in as parameters: `source('db', { sql: 'select … where at >= $1 and at < $2', params: [event.previous, event.due] })`.

A report from CoDNA's own pages, the week's decisions:

```js
export default function ({ event, codna, actions }) {
  const day = event.due.slice(0, 10)
  const into = 'output:reports/decisions/' + day
  const title = 'Decisions, week to ' + day
  // The template takes one of the 20 pages an ask may give.
  const pages = codna.find({ space: 'wiki', pathPrefix: 'decisions', updatedAfter: event.previous, limit: 19 })
    .filter((p) => p.updatedAt < event.due)
  if (pages.length === 0) {
    actions.write({ into, title, content: 'No decision was recorded or changed this week.\n' })
    return
  }
  const listed = pages.map((p) =>
    '- ' + p.title + ', ' + (p.createdAt >= event.previous ? 'new' : 'changed') + ': link it as /' + p.address.replace(':', '/') + '.md')
  actions.log('The model is given:\n' + listed.join('\n'))
  actions.ask('Write this week\'s decisions report as the template says, from the pages given.\n\n' + listed.join('\n'),
    { documents: ['wiki:templates/weekly-decisions', ...pages.map((p) => p.address)], into, title })
}
```

- **Give the model only what the report needs.** The prompt and its pages go to the model's provider. An ask gives at most 20 pages, by address in `documents`, and 512 KB with its prompt; the template is one of them. Leave out rows, columns or pages the report doesn't use.
- **Links:** a report links to a page as `/wiki/<path>/<slug>.md`, which works from any folder. Give the model each link, as above.
- **A period with nothing to report** gets a one-line page from `actions.write`, and no ask, so it costs nothing.
- **Log what the ask is given** with `actions.log`. A dry run shows only how many pages and bytes an ask would send, and the log is how the person sees what goes in.
- **The schedule is the person's:** `schedule` such as `30 8 * * 1-5`, and `timezone` such as `Asia/Tbilisi`.
- **The model:** leave `model` out to use the instance's default. Set it only when the person wants another, chosen from `list_models`, which gives each one's price.
- **Describe it:** say what it reports, for whom, at whose request, and on which date. `create_automation` registers it switched off.

## 3. Agree the report, from a real preview

1. **Write the report's template as a page,** such as `wiki:templates/weekly-decisions`: its sections, what each says, how numbers and changes are shown, how long it is. Ask the person what they want in it.
2. **Dry-run it:** `run_automation` with `dry_run: true`. A tick run by hand stands for the latest tick due, and `at` runs it as of a time that has passed: pick one whose period had something in it. A dry run reads CoDNA but not sources, so a script that stops when a source's read failed stops there. Check that the ask would be made, and what the log says it would be given.
3. **Preview it:** `run_automation` with `preview: true`, as a tick (with `at`, as above), or on one document for one fired by events. The sources are read and the model writes the page for real; the page is written nowhere. A preview's asks cost what any ask does (a few cents with the default model), from everyone's budget: ask the person before the first one. Then show them the page it returns.
4. **Change the template or the prompt until they accept it,** and preview again after each change. Preview only as often as a change needs.

While asking is switched off or the budget is spent, a preview's ask is refused like any other, and the report cannot be agreed yet. Show the person the template and what the dry run says the model would be given, say who switches asking on, and stop there: the agent stays off.

## 4. Ask who should read it

- **Reports go in a folder of the owner's,** such as `output:reports/decisions/2026-11-02`. Each is private until it is shared. The owner finds every report in that folder, and the latest on the agent's card on the Agents page.
- **Others should read them?** Share the folder once: `preview_share` on the folder, show the person what it covers, and after their yes, `share_folder` with those people or groups. Each new report reaches them as it is written, in the web UI and their inbox. That is one share, not one per report.
- **They want it in Slack or by email?** A second automation, fired by `document.created` in that folder, can post the page to a connection or email members. Set that up with the `set-up-an-automation` skill.

## 5. Switch it on

- **Only after the person has accepted a preview's page**, with `set_automation_enabled`.
- **Then tell them** where the reports will appear (the folder, the Agents page, and the inbox of everyone it is shared with), when the first is due (its `next_run_at`), and the agent's `url`, where they can try it and switch it off.

## When it isn't working

- **Read `automation_runs`.** Each run says how its ask went: queued, written with the page and its cost, or failed and why. The usual reasons are the month's budget being spent, asking being switched off, or the model no longer being allowed.
- **A read that failed** is in the run's reads, with the source's own words.
- **A report that reads wrong** is a template to change: change it, and preview again.

## Never

- Never ask for, or write down, a token, a key or a password. Sources and keys are pasted in CoDNA's own pages.
- Never preview before the person agrees to what it costs, and never more often than a change needs.
- Never switch an agent on before the person has seen a preview's page and said yes.
- Never share the reports folder before `preview_share` and the person's yes.
