# 0029 · An automation keeps what it needs between runs: a little state, given to each run and changed by its actions

**Status:** Accepted · 2026-11-01. It adds to the script contract
([automations.md](https://codna.me/docs/automations.md#the-script-contract)). The sandbox
([0007](https://codna.me/docs/adr/0007-quickjs-sandbox.md)) does not change: each run still starts in a fresh runtime, and the
state is something the host keeps and hands it, like `inputs`.

## Context

A run knows what started it, what it may read in CoDNA, and what its sources answer
([0025](https://codna.me/docs/adr/0025-people-own-connections.md)). It knows nothing about the runs before it. Time is the one
exception: a scheduled tick is given its period (`event.previous` to `event.due`), so a report
covers each day once without remembering anything.

Much of what people now automate needs more than time:

- **A cursor that isn't a time.** "Tell support about each new ticket" against an API that pages by
  id (`since_id=4812`), not by date. Without the last id seen, every run either repeats tickets or
  misses them.
- **Not doing it twice.** An incoming request ([0027](https://codna.me/docs/adr/0027-other-apps-start-automations.md)) is
  known by its delivery id. But "a deal is won" can arrive from two systems, or twice a day, and
  "post it to Slack once per deal" needs the deals already posted.
- **Counting.** "Tell me when a form gets its 50th sign-up", or "when the same check fails three
  times in a row".
- **Work over many runs.** A sync of 2,000 rows, at 20 calls a run
  ([0028](https://codna.me/docs/adr/0028-scripts-call-the-apis-they-read.md)), needs to know where the last run stopped.

Today a script can only keep these in a page: write it in `output`, read it back next run. That
works, and it is wrong. The page is for people: it shows in search and in the owner's folders, and
every change is a revision. It costs one of the run's 20 reads and an `actions.write` of up to
256 KB. And a script that loses the race between two runs writes over the other's page.

n8n has the same gap, and fills it with "workflow static data", or a database node.

## Decision

### Each automation has a state

- **A JSON object, kept by CoDNA for that automation,** at most 64 KB. It is empty when the
  automation is made.
- **Each run is given it as `state`,** frozen, as it was when the run started. The `inputs` export is
  given it too, so a read can start from a cursor:

```js
export function inputs({ state }) {
  return { tickets: source('helpdesk', { path: '/tickets', query: { since_id: state.lastId || 0 } }) }
}

export default function ({ state, inputs, actions }) {
  const fresh = inputs.tickets.data.tickets
  for (const t of fresh) actions.notify('group:support', 'New ticket: ' + t.subject)
  if (fresh.length) actions.remember('lastId', fresh[fresh.length - 1].id)
}
```

### Changed by actions, like everything else a run does

- **`actions.remember(key, value)`** sets one key to any JSON value. `null` takes the key out.
- **`actions.add(key, number)`** adds to a number, starting from 0. The addition is made by the
  database, so two runs counting at once both count.

Both are proposed and applied with the run's other actions, in its one transaction. So:

- **a run that fails changes no state**, and a run's state and what it did agree;
- **a dry run and a preview change none,** and say what they would remember;
- each counts as one of the run's 50 actions.

A change that would take the state over 64 KB is refused, saying so, and the run's other actions
go ahead, as any refusal does.

### Runs at the same time

Two runs of one automation may run at once: events come together, or a run by hand meets a tick.
Each is given the state as it was when it started. Their changes are made key by key: `remember`
on different keys keeps both, and `add` keeps both counts. Two runs that `remember` the same key
leave the value of the one that finished last. A script that must not lose a value keeps it under
a key of its own, such as `seen.<id>`.

### Who sees it

- **Its owner,** as they see its script: on the automation's page, under **What it keeps**, and from
  `get_automation` over MCP. Management never sees it, as it never sees a script.
- **Its owner can clear it,** all of it or one key, on that page or with `update_automation`. A
  person may need to start a sync over, or a count again.
- **Each run's record keeps the state it was given,** cut at 64 KB, for 30 days, as it keeps what its
  reads answered. The record also keeps each change it made, in its actions. A run can still be
  replayed from its record, which is why `Date` and `Math.random` are withheld (0007).

### What happens to it

- **It goes with its automation.** Deleted, the state goes too. Handed to someone else when its
  owner leaves, it goes with it, as the script does.
- **It is not a page.** It isn't searched, has no history and isn't shared. What people should
  read still goes in a page.

## Consequences

- **The commonest reason to fake state in a page goes away,** and with it the clutter of
  bookkeeping pages and the lost-update race.
- **Scripts get harder to read.** What a run does now depends on what earlier runs kept. The
  automation's page shows the state, a run's record shows what it was given, and a dry run says
  what it would change.
- **Data from outside can sit in CoDNA for as long as the automation exists:** ids, counts, at most
  64 KB of whatever a script keeps. It is the owner's, cleared by them, and deleted with the
  automation.
- **No new limit on what a run reads:** the state is given up front, like the event, not read
  through `codna.get`.

## Alternatives rejected

- **Pages, as today:** they are for people, they are noisy, they cost a read and a write, and two
  runs overwrite each other.
- **The script returns its new state:** a run would replace the whole state, so two runs at once
  would lose one's changes, and counting would be impossible.
- **A key-value store read during the run,** `kv.get(key)`: a host call per read, against the run's
  20 reads. And what a run read could no longer be told from its record, unless every read were
  recorded too.
- **One state shared by a person's automations:** it ties scripts together, so one can't be read
  without the others. Automations that must share something share a page.
- **An unlimited store, or a database connection CoDNA keeps for scripts:** that is a product of its
  own. A team that needs one has a `postgres` source (0025).

## Settled when it was accepted

Tornike accepted it as proposed on 2026-11-01, settling:

1. The names: `state`, `actions.remember(key, value)` and `actions.add(key, number)`.
2. The size: 64 KB per automation.
3. Its owner alone sees it, on its page and over MCP, and clears it.
4. A run's record keeps the state it was given, cut at 64 KB, for 30 days.

## Still open

- **A deadline on a key,** such as "forget a seen id after a week", so a set of seen ids doesn't
  grow to the limit. Until then, a script keeps a bounded list.
- **State shared by automations,** if a real need appears that a page doesn't meet.

## Building it

1. **The state:**
   - kept per automation;
   - given to `inputs` and to the run;
   - `remember` and `add`, applied in the run's transaction, with the limit;
   - dry runs and previews;
   - what a run's record keeps;
   - the automation's page (What it keeps, and clearing it);
   - `get_automation` and `update_automation`, and the contract.
2. **Docs:** [automations.md](https://codna.me/docs/automations.md), and the `set-up-an-automation` skill.
