Skip to main content
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.

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: 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, 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:
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.
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.

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:
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, 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.

Do and Don’t

Promo Product Selection

The full worked example: a promo selection Agent with a non-repetition filter

Agent Memory

The per-user personalization layer — and why it isn’t workflow state

Queue

Cases, statuses, and handing work between Agents

Designing Human-in-the-Loop Workflows

Choosing approval shapes and escalation behavior

Scheduling Agents

Running an Agent on a recurring cadence