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.
| Stage | What happens | Owner | Wikantik example |
|---|---|---|---|
| Sense | capture from external sources and from the product's own emitted telemetry; retain history | plane | Google Search Console (GSC) / Bing / Yandex polling → webmaster_* gauges + daily snapshots; Wikantik retrieval-quality logs |
| Signal | source-generic detection needing no product knowledge; ship as evidence | plane | visibility/opportunities.py (content_gap, decay), the measured Click-Through Rate (CTR)-by-position curve → POST /admin/insights/ingest |
| Join + decide | join shipped evidence with product data; rank, gate, calibrate; produce a backlog and a list of what was suppressed | product | OpportunityEngine over search_visibility_snapshot + agent-gap demand + page facts; traffic gate; WeightCalibrator |
| Act | an agent picks from the backlog and acts only through the product's scoped surfaces; records the prediction | agent runtime (via product) | list_content_opportunities → edit page → snooze_opportunity; content_change_log.predicted_priority |
| Measure + learn | after a settle window, compare realized outcome with the prediction against a control; feed the result back into ranking | product | nightly 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 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.
These are the lessons the first loop paid for; they are mandatory, not stylistic.
suppressed. Do not lower a gate to make a backlog non-empty.)insufficient_data honestly rather than forcing a number.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.
The first fully-built loop, shipped 2026-08-16/17 (Wikantik 2.4.4–2.4.10, jakemon 8ba33fe). Full design: ContentIntelligenceDesign.
(as_of, engine, key) upsert using a dedicated jakemon-shipper account holding only an admin:insights scoped grant.OpportunityEngine runs four rule types (agent_gap from the product's own retrieval logs; engine_divergence, vocabulary_gap, stale_high_traffic from shipped search data joined to page facts), behind the traffic gate, with priorities calibrated by realized effect.list_content_opportunities (returns opportunities, suppressed, uncalibratedTypes, ctrCurveSource) and snooze_opportunity; edits go through the normal page-write surfaces, which record predicted_priority.EffectEvaluator scores each change after the settle window with difference-in-differences against site trend; WeightCalibrator adjusts rule weights only once a type has enough non-insufficient_data verdicts.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.
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.
| Stage | Roller (sketch) |
|---|---|
| Sense | plane 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 |
| Signal | an 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 + decide | Roller 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) |
| Act | an agent drafts or edits a post / a proactive customer note through Roller's scoped write surface, records the predicted engagement lift |
| Measure + learn | after 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.pyis 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.
(as_of, key), provenance keptinsufficient_data allowed