Simple Agility Feedback Loop Pattern

Every product in the Simple Agility stack optimizes itself with the same loop: the observability plane senses and retains, the product joins that against its own data and decides, an agent acts through the product's surfaces, and the product measures whether the action worked. This page names the loop so that a new product's agent builds this shape and does not re-invent — or, worse, re-implement in its own backend — the plane's half. It is a pattern with a checklist, not a shared library: Roller's loop joins different data than Wikantik's and should not inherit Wikantik's code.

The five stages and who owns them

StageWhat happensOwnerWikantik example
Sensecapture from external sources and from the product's own emitted telemetry; retain historyplaneGoogle Search Console (GSC) / Bing / Yandex polling → webmaster_* gauges + daily snapshots; Wikantik retrieval-quality logs
Signalsource-generic detection needing no product knowledge; ship as evidenceplanevisibility/opportunities.py (content_gap, decay), the measured Click-Through Rate (CTR)-by-position curve → POST /admin/insights/ingest
Join + decidejoin shipped evidence with product data; rank, gate, calibrate; produce a backlog and a list of what was suppressedproductOpportunityEngine over search_visibility_snapshot + agent-gap demand + page facts; traffic gate; WeightCalibrator
Actan agent picks from the backlog and acts only through the product's scoped surfaces; records the predictionagent runtime (via product)list_content_opportunities → edit page → snooze_opportunity; content_change_log.predicted_priority
Measure + learnafter a settle window, compare realized outcome with the prediction against a control; feed the result back into rankingproductnightly EffectEvaluator (difference-in-differences against site trend, 28-day settle) → calibration

The direction of travel: over time signal logic shifts left from the plane into the product that has the domain context; capture stays in the plane. If a piece of logic needs the product's tables or vocabulary, it is a product stage.

The loop needs product-owned tables, and that is not a contract violation

The plane page's "do not build this in your backend" table is about observability infrastructure. A loop's inputs are different: retrieval_query_log (what agent_gap reads), search_visibility_snapshot (the shipped feed's landing table), content_change_log (predictions and verdicts), content_opportunity_seen (first-seen provenance) are all correct product tables — they are functional inputs to the product's own behavior, not telemetry for observation. The deciding rule lives on the plane page; apply it before concluding a table is redundant with Loki. The same event often produces both a log line and a row, on purpose.

Three properties every product loop must have

These are the lessons the first loop paid for; they are mandatory, not stylistic.

  1. Suppression is a first-class output. When a rule does not run — not enough traffic, no fresh feed, gate closed — the loop must say so, with the measured and required values, on the same surface as the backlog. An empty backlog with suppressed entries means "not enough evidence to look", which is a completely different statement from "nothing to do", and the difference is invisible if a consumer reads only the list. (Wikantik: three of four native rules sit behind a site-level traffic gate — 5,000 impressions per 28 days required, ~2,650 measured — and report themselves in suppressed. Do not lower a gate to make a backlog non-empty.)
  2. Every action records a prediction. An action without a stored expected effect cannot be measured, and an unmeasured loop cannot calibrate — it just keeps guessing with more confidence. Store the predicted value at act time; evaluate against a control (site trend, holdout, or difference-in-differences), never against the raw before/after; mark verdicts insufficient_data honestly rather than forcing a number.
  3. Provenance on every signal. Each shipped row and each derived number carries as_of, source and engine, and every read surface reports which inputs it used and how fresh they were (Wikantik: ctrCurveSource: imported:<date> versus builtin). A feed going quiet must be visible, not merely inferable from the numbers looking different.

Supporting rules: minimum-support thresholds are stated per rule and are conservative (small sites cannot support per-page statistics — Wikantik's median page sees two impressions in 28 days); snapshots are selected, never summed across days; uncalibrated priorities are labelled as such so an agent weighs them less.

Worked example: Wikantik Content Intelligence

The first fully-built loop, shipped 2026-08-16/17 (Wikantik 2.4.4–2.4.10, jakemon 8ba33fe). Full design: ContentIntelligenceDesign.

What the plane learned from it: Bing converts ~6× better per impression than Google on this corpus (1.85% CTR at position 4.7 versus 0.32% at position 36) — a plane-level fact any product's SEO loop can use, which is exactly why capture lives in the plane.

Sketch: a Roller communication loop (hypothetical — nothing here is built)

Roller's loop will look strikingly like Wikantik's because it, too, is fed by the anticipated Search Engine Optimization (SEO) feedback loop — plus its own communication signals.

StageRoller (sketch)
Senseplane captures Roller's event lines (post_published, email_sent, email_opened, email_bounced, reply_received, unsubscribe) via the telemetry contract, and search visibility for the blog's site_host from the same GSC/Bing/Yandex feed Wikantik uses
Signalan aggregated engagement feed (per post, per send: opens, bounces, replies by as_of) plus the same search opportunities/CTR curve, sliced to Roller's site_host
Join + decideRoller joins engagement + search visibility against its own posts/subscribers/segments: "topic X gets search impressions but no post", "posts of type Y bounce/unsubscribe more", "question Z recurs in replies with no post answering it" → ranked communication opportunities with a suppression list (a small list cannot support per-segment statistics any more than a small site supports per-page ones)
Actan agent drafts or edits a post / a proactive customer note through Roller's scoped write surface, records the predicted engagement lift
Measure + learnafter a settle window, realized opens/replies/search clicks against a control (other posts' trend), verdicts feed weights

Do not read the Signal row as an available capability. The engagement feed does not exist, and neither does a generic mechanism to build one: ship_visibility.py is bespoke to search visibility, so today a second feed means a second hand-written shipper. Roller's loop is therefore blocked on two plane work items — the generic aggregate-and-ship path and the engagement feed itself — both sequenced in SimpleAgilityRoadmap. Follow the plane page's Requesting a new feed process; do not work around the gap by polling from Roller.

What Roller must not build: a poller for any third-party source, an SLO calculator, or an events table whose only purpose is engagement reporting. What it must build: its own tables for the shipped feeds and for its loop's inputs, the joins, the ranking with suppression, a scoped agent-facing write surface, and effect measurement.

Checklist for a new product loop

See also