CoDNA Request access
Contents

Docs Decisions

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). The sandbox (0007) 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). 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) 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), 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:
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, and the set-up-an-automation skill.