> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duvo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Giving Recurring Runs a Memory

> Recurring Agents that pick from a pool will repeat themselves unless you design where their memory lives. Externalize state between Runs, exclude at the source, and validate before writing.

A scheduled Agent that selects items from a pool each cycle — promo products, outreach targets, audit samples, content topics — has a failure mode unique to recurring work: every Run starts fresh, so nothing stops this cycle from picking exactly what the last cycle picked. The same applies to any recurring workflow whose next Run depends on what previous Runs already did.

This page covers the design principles that give recurring Runs a working memory and keep their output trustworthy. For a full worked example that applies all of them, see the [Promo Product Selection playbook](/user-guide/playbooks/merchandising/promo-product-selection).

## Each Run Starts Fresh — Decide Where State Lives

Runs do not share context with each other. Whatever the next Run needs to know, the current Run has to record somewhere durable, and the AOP has to say where to look for it. Duvo gives you three places to keep state, each for a different purpose:

| Where                                                                         | What it's for                                                                            | What it's not                                                                     |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| [Agent Memory](/user-guide/assignment-features/assignment-memory)             | Per-user preferences and thresholds, applied to every Run that user starts               | A record of what past Runs did — it personalizes behavior, it doesn't log history |
| An external system of record (a tracker sheet, an archive folder, a database) | Workflow state: what was already selected, sent, or processed                            | Automatic — the AOP must say where it lives and when to read and write it         |
| [Case](/user-guide/assignment-features/case-queue) data                       | Progress on one work item as it moves through a Queue, surviving postpones and handovers | State shared across the whole workflow — it's scoped to that Case                 |

For "don't repeat what previous Runs did," the middle row is the one that matters: put the workflow's history in a system of record your team can open, audit, and correct — a Google Sheet, a Drive folder, a database table. If a person can't see the state, they can't fix it when it's wrong.

## Make the Archive the Memory

The strongest version of externalized state is when the workflow's own output is its memory. In the [promo selection playbook](/user-guide/playbooks/merchandising/promo-product-selection), each cycle writes its shortlist to an archived sheet, and the next cycle builds its exclusion list by reading the last N archived sheets. There is no separate "already used" list to maintain, because the archive is the list.

This closes the loop by construction:

* **Self-maintaining** — producing output and updating memory are the same action, so the memory can't silently go stale
* **Auditable** — anyone can open the archive and see exactly why an item was excluded
* **Correctable** — a human can delete a row to make an item eligible again, without touching the Agent

Keep an index (a tracker sheet listing the archived outputs in order) so the Agent knows where the last N outputs are without scanning a whole folder.

Deriving state from the archive also survives missed cycles. A scheduled tick that can't run — the previous Run is still active, or a Connection is missing — is skipped, never backfilled. Memory based on what the calendar says should have happened drifts the first time a cycle is skipped; memory derived from archived outputs always reflects what actually ran.

## Exclude at the Source, Not After

Apply non-repetition filters inside the source query — a `NOT IN` clause in the `WHERE` — rather than fetching results and filtering them afterward:

```sql theme={"dark"}
AND sku NOT IN ('SKU-001', 'SKU-042', 'SKU-107')
```

Filtering at the source means ranking and limits operate on the already-eligible pool, so you always get a full result set, and excluded items never appear in intermediate output the Agent might act on. Post-filtering has the opposite properties: the limit fills up with items you're about to throw away, and one missed filter step leaks an excluded item into the output.

<Warning>
  An exclusion filter that matches nothing fails silently — the query succeeds and the output looks normal, but nothing was excluded. The two usual causes: an empty list (`NOT IN ()` is invalid SQL — omit the clause instead) and identifier format drift between the archive and the source (`SKU-001` vs `001` — normalize before comparing). Test both cases explicitly before going live.
</Warning>

## Validate Before You Write

Recurring workflows drift. The exclusion window grows, the eligible pool shrinks, an upstream table changes — and one cycle, the query quietly returns less than the workflow needs. Add a validation gate between reading and writing: if the result doesn't match expectations (too few rows, missing fields, out-of-range values), stop and request human review instead of writing output.

Partial output is worse than no output, because downstream consumers can't tell partial from complete — a pricing Agent fed half a shortlist prices half a promotion with no warning. A hard stop with a clear question ("\[actual] of \[expected] returned — shrink the shortlist, narrow the exclusion window, or widen the scope?") turns silent drift into a decision someone actually makes.

## Keep the Selection Logic in the AOP

Ranking expressions, thresholds, and exclusion windows belong in the AOP — not in a spreadsheet formula, a saved query, or the head of whoever ran the process last. In the AOP, the logic is versioned with the Agent, visible to anyone who opens it, and changed by deliberate edit rather than drift. When the reviewer asks "why did it pick these?", the answer is readable in one place.

## Let Reviewers Adjust, Not Redo

An approval gate that only offers approve or deny forces the reviewer to reject a whole batch over one bad item — so they stop rejecting, and the gate stops working. Instead, design the approval to accept small corrections in the response. Responses to requests are free text, so the AOP can define simple commands:

```
Support these commands in the reviewer's response:
- "Remove: [ID]" — take that item out of the result and confirm what changed.
- "Add: [ID]" — look the item up, add it, and confirm.
Process all commands before treating the response as a final approval.
```

Have the Agent confirm each command back before treating the response as final, and make sure whatever happens after approval reads the post-command result — not the originally proposed one. And if the post-approval step creates Cases in a [Queue](/user-guide/assignment-features/case-queue), make it idempotent: Queues do not deduplicate Cases, so a retried Run or a twice-processed approval creates duplicates unless the AOP checks for an existing Case first. For choosing the right approval shape and escalation behavior, see [Designing Human-in-the-Loop Workflows](/user-guide/assignment-features/hitl-design).

## Do and Don't

| Do                                                                       | Don't                                                                      |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| Write workflow state to a system of record the team can open and correct | Assume a Run knows what previous Runs did                                  |
| Derive "already done" lists from archived outputs                        | Maintain a separate exclusion list someone has to remember to update       |
| Apply exclusions in the source query, before ranking and limits          | Fetch everything and filter afterward                                      |
| Stop and ask when results don't match expectations                       | Write partial output and let downstream Runs discover it                   |
| Keep ranking logic and thresholds in the AOP                             | Encode selection logic in spreadsheets or saved queries outside the Agent  |
| Accept reviewer corrections inside the approval response                 | Force reviewers to approve or reject entire batches                        |
| Check for an existing Case before creating one after approval            | Assume the platform deduplicates Cases or backfills skipped schedule ticks |

## Related

<CardGroup cols={2}>
  <Card title="Promo Product Selection" icon="shopping-cart" href="/user-guide/playbooks/merchandising/promo-product-selection">
    The full worked example: a promo selection Agent with a non-repetition filter
  </Card>

  <Card title="Agent Memory" icon="brain" href="/user-guide/assignment-features/assignment-memory">
    The per-user personalization layer — and why it isn't workflow state
  </Card>

  <Card title="Queue" icon="layers" href="/user-guide/assignment-features/case-queue">
    Cases, statuses, and handing work between Agents
  </Card>

  <Card title="Designing Human-in-the-Loop Workflows" icon="workflow" href="/user-guide/assignment-features/hitl-design">
    Choosing approval shapes and escalation behavior
  </Card>

  <Card title="Scheduling Agents" icon="clock" href="/user-guide/assignment-features/scheduling-assignments">
    Running an Agent on a recurring cadence
  </Card>
</CardGroup>
