# 0031 · A step can run later: the same automation, at a time its run chose, given what it asked to be handed back

**Status:** Accepted · 2026-11-02. It extends the next steps of
[0030](https://codna.me/docs/adr/0030-a-calls-answer-runs-the-next-step.md), which run as soon as a call is answered, to steps
that run at a time a script chooses. The sandbox ([0007](https://codna.me/docs/adr/0007-quickjs-sandbox.md)) does not change.

## Context

An automation now runs on an event, on a schedule, on a request from another app
([0027](https://codna.me/docs/adr/0027-other-apps-start-automations.md)), or on a call's answer (0030). Each runs now. Many
flows have a step that should run later, relative to something that happened:

- **A sign-up arrives:** welcome them now, and check in three days whether they made their first
  page.
- **An invoice is sent:** if it isn't paid in 7 days, remind them; in 14, tell the account owner.
- **A ticket is opened:** if nobody has answered in 2 hours, tell the support lead.
- **An API says to try in an hour,** or a report should wait until a sync it started is done.

What exists can't do it well:

- **A schedule** runs at fixed times, not "three days after each sign-up".
- **A frequent schedule and the state** ([0029](https://codna.me/docs/adr/0029-an-automation-keeps-what-it-needs-between-runs.md))
  can fake it. The script keeps a list of what is due and a cron checks it every few minutes. That is
  hundreds of runs a day doing nothing, a list that hits 64 KB, and code that every such script
  writes again.

n8n's answer is a Wait node holding a running workflow. Here, a step is a run, and a run is 100 ms,
so the waiting is the host's.

## Decision

### `actions.later`: run this automation again, at a time

```js
// A sign-up: welcome now, and check on them in three days.
actions.email(form.email, 'Welcome', '…')
actions.later('3d', { step: 'check-in', email: form.email })

// Three days on:
if (event.type === 'later' && event.then.step === 'check-in') { … }
```

- **When:** a duration from the run, `'30m'`, `'2h'` or `'3d'`, or a time that is still to come, such
  as `'2026-11-12T09:00:00+04:00'`. At least a minute ahead, at most 90 days. Scripts have no
  `Date`, so a duration is counted from the run's `event.at`.
- **`then`:** any JSON, at most 16 KB, handed back as it was given, as a call's is (0030).
- **The step** is told `event.type` `'later'`, `event.then`, `event.step`, and `event.due`, the time
  it was asked for. It runs within a minute of that time, as the same automation and owner, with its
  current script and state. Like any run, it can call, remember, and ask for later again.
- **It counts as a step of its chain,** as a call's answer does: a chain runs at most 10 steps in all
  (0030). "Remind every day until it's paid" belongs on a schedule.

### One waiting step per key

`actions.later(when, then, { key })` names a waiting step. Asking for later again with the same key
replaces the one waiting:

- **"Remind them in 2 hours unless they answer"** asks again on every answer, and only the last
  reminder stays.
- **A step waiting for something that has happened** is replaced, or checks when it runs and does
  nothing.

Without a key, every later waits.

### What is kept, and who sees it

- **Its owner sees what waits,** on the automation's page under **Waiting**: when each is due, its
  key, its `then`, and the run that asked for it. They cancel one there, or all, and over MCP with
  `get_automation` and `update_automation`. Management never sees them, as it never sees a script.
- **At most 1,000 waiting per automation,** and 20 asked for a run, within its 50 actions. A later
  past either is refused, saying so.
- **The step's run record keeps its `then`,** and the run that asked for it, as a call's next step
  keeps its answer (0030).
- **A dry run and a preview ask for nothing.** They show each later with the time it would run.
  **Try it later…** on the automation's page, and `run_automation` with `later` and `then`, run a
  step now, as if its time had come.

### When it can't run

- **A step due while its automation is switched off is dropped,** and the Waiting list says so, as a
  call's next step is (0030). Switched on again, the steps not yet due still wait.
- **Deleting the automation** deletes what waits. **Handing it to someone when its owner leaves**
  hands its waiting steps over too, but they arrive switched off, as the automation does, so they are
  dropped as they come due unless it is switched on.

## Consequences

- **Delays relative to an event are one line,** not a schedule, a list in the state, and a check
  every few minutes.
- **Automations run when nothing visible has just happened.** The Waiting list says what will run
  and when, and each such run's record says which run asked for it.
- **Up to 1,000 waiting per automation.** The scheduler finds them as it finds due schedules, in
  Postgres, so a restart loses none.
- **Switching an automation off drops what comes due.** That is the price of "off means off". The
  page says so before switching off one that has steps waiting.

## Alternatives rejected

- **A Wait in the script,** `sleep('3d')`: a run is 100 ms and keeps nothing in memory between runs.
- **A schedule per item:** a cron for each sign-up is an automation per sign-up.
- **The state and a frequent schedule,** as today: many empty runs, a list against the 64 KB limit,
  and the same code in every script.
- **Keeping steps due while the automation is off, to run when it is switched on:** a reminder for
  last week would arrive late, and "off" would no longer mean nothing happens.
- **Waiting for a person,** "until someone approves": that is waiting on an event, which an event
  automation already is. A page tagged `approved` starts it, with the state saying what was waiting.

## Settled when it was accepted

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

1. The shape: `actions.later(when, then, { key })`, and `event.type` `'later'`.
2. The limits: at least a minute, at most 90 days ahead; 1,000 waiting per automation, 20 asked
   for a run; within the chain's 10 steps.
3. A step due while its automation is off is dropped, and the page says so.
4. Asking again with a key replaces the step waiting under it.

## Still open

- **Waiting on a person** in a step's own shape, if "an event automation and the state" proves
  awkward in practice.
- **A run at a time of day in the person's own time zone,** such as "9:00 the next working day",
  rather than a duration or a fixed time.

## Building it

1. **Later:**
   - `actions.later` and the steps waiting, with their keys and limits;
   - the scheduler running them as they come due;
   - `event.type` `'later'`, with `event.then`, `event.step` and `event.due`;
   - dropping a step that comes due while its automation is off;
   - the Waiting list and cancelling, on the page and over MCP;
   - trying a later step now, and the contract.
2. **Docs:** [automations.md](https://codna.me/docs/automations.md), and the `set-up-an-automation` skill, with the
   sign-up check-in as its example.
