Find a workflow worth learning
A workflow-first quickstart — connect your traffic, let ultrarouter surface the two a-ha payloads, read the proposed learning plan over the API, and confirm one workflow. No five-toggle onboarding; nothing is learned until you confirm.
Flywheel is workflow-first. You do not fill in a five-toggle form to get started — you point your existing app at ultrarouter, let it read the calls you already make, and it surfaces one workflow worth learning with a complete, inspectable plan. Your only decision is to confirm what "it worked" means for that workflow. Until you do, nothing is learned.
This quickstart walks that path against a local pnpm dev:e2e stack: connect, see the
structural a-ha, read the semantic a-ha as a proposed
learning plan over the API, and confirm it. It makes zero paid model calls —
discovery reads your existing traffic and moves metadata, it does not send new prompts.
Minute zero: you do not have to wait for traffic
You do not have to send a single request to see a proposal. From your public website alone, ultrarouter infers the workflows you run and a provisional definition of good, and shows them at minute 0: we think you run these workflows and good means X — confirm all, edit, or skip. This is the plug-and-play path.
- Minute 0 (inferred). From the site brief, a bounded list of inferred workflows
(up to 8), each with a provisional quality bar, cost budget and outcome channel — clearly
labelled inferred, not measured. They sit beside any workflow you declare through the
SDK/manifest. Signup analysis is multi-page: it fetches several same-origin pages of
your own site (through the one SSRF egress guard — cross-origin links are dropped before
any fetch), runs several small-model categorise passes per page against a closed
workflow taxonomy — each pass presents the taxonomy in a different order so the passes
cancel the model's list-position bias — and merges the recorded runs into the list: a
workflow is kept only when a majority of a page's runs agree on it (so one stray or
injection-steered run can never add a false positive), and its support is the number of
pages that agree. Deterministic, with the pages that evidence each workflow recorded. It runs once, under a stated
one-time signup-analysis budget — separate from your run-time spend cap and from the
analyzer's monthly discovery cap; a changed site re-runs it, an unchanged site recomputes
and spends nothing. The
GET /v1/workspaces/:id/site-briefstatus prints that budget (analysis.budget) and the per-page status (analysis.pages— analysed / skipped-cap / fetch-refused / budget-exhausted / flagged-injection / screen-failed); it never returns the page text. Your site is untrusted data: each categorise answer is schema-checked to the closed taxonomy and every inferred workflow is a proposal you confirm, so a hostile page can steer nothing but a suggestion. - Minute 15 (traffic-backed). After your 20th request, the proposal is revised from your own traffic — now labelled from your traffic.
- Day 1–3 (semantic). The full semantic a-ha — the workflow's measurement bundle, alternatives and the one-sentence preview.
Each proposal carries its evidence basis (an inferred proposal is never presented as
measured), and Confirm all (N) is the one-tap primary action — it states the exact
count it will confirm. Clicking it is your confirmation — nothing auto-confirms. It
confirms every proposal through the same per-plan confirm path used below, so the label
field and the decision contract apply to each; the inferred site-brief proposal is
confirmable too (its objective is reconstructed from the inferred intent, and it compiles to
audit_only — a proposed objective and an unobserved channel never teach).
Per proposal you can also Confirm (just that card), Edit, or Skip. Skip
removes a workflow from Confirm all — you never confirm something you declined; if we later
revise that workflow from new evidence it returns, marked revised since you skipped. Edit
stages what "better" means for that card, and Confirm all (or the per-card Confirm) then
confirms your edited choice, not the default. The server re-reads the eligible set when you
press Confirm all, so a stale page can never confirm a card you skipped. How many workflows
you see depends on your evidence: the multi-page site analysis seeds a bounded list (up to 8)
from your site alone, and declared and traffic-derived sources add to it.
What the minute-0 screen shows
The screen is one deduplicated list — the multi-page analysis's specific workflows replace the single generic "your product workflow" seed when both exist, so you never see the same workflow twice.
Each card shows what we inferred and from what, so an inference always reads as an inference (never as a measurement):
- The workflow label — the category we detected (for a site-inferred workflow) or the call type (for a declared one), with its provisional good means X (the quality bar, the outcome channel, and the window).
- An evidence basis chip — From your website (inferred) or From your traffic (declared).
- Why we inferred this — for a site-inferred workflow, the pages of your own site that agree on it and how many independent reads agreed ("Pages that agree: 2 · Agreeing reads: 5"), listing each page by path. We never show the page text — only the path, a count and a hash.
Below the cards, a calm "pages we skipped" section names, in plain words, every page the
analysis did not use to infer your workflows — a page that looked like instructions aimed
at us (flagged_injection), one we could not safely screen (screen_failed), one the
one-time analysis budget ran out before, or one with not enough agreement to use it
(insufficient_runs) — each with its closed reason codes. Skipping is non-destructive:
nothing is removed, and a skipped page steers nothing. The section also states the one-time
analysis budget the whole pass ran under.
How we treat your site as untrusted
Every page we fetch is untrusted data, never an instruction. The primary controls are structural: your page rides inside a neutralised data envelope, each categorise answer is constrained to a closed workflow taxonomy (a slug we do not offer is dropped), and a workflow enters your list only when a majority of a page's independent runs agree — so a single steered run changes nothing. On top of that, each fetched page is run through a dedicated injection screen before it is categorised. The screen is two layers:
- A deterministic pre-screen that runs on the raw HTML and the extracted text and catches
the mechanically detectable attempts — instructions hidden in HTML comments or
display:none/aria-hiddenblocks, base64-encoded or zero-width / homoglyph-obfuscated payloads, and self-labelling claims like "already verified" or "the analyzer must…". - A model screen whose only output is a verdict (
cleanorsuspected_injection).
A page either layer flags is marked flagged_injection, excluded from categorisation,
and named to you in analysis.pages with a set of reason codes (never the page text). A
page we could not screen (a failed screen call) fails closed — it is not categorised
either, and is named screen_failed. A payload split across two pages never assembles:
each page is screened and categorised on its own, with no cross-page context. And the screen
calls draw on the same one-time signup-analysis budget as categorisation, so a page that
says "analyse a thousand pages" or "spend your budget" moves nothing — the crawl cap, the
budget, the closed taxonomy and majority agreement already bound it.
Prerequisites
- The stack running. Locally,
pnpm dev:e2eboots the gateway onhttp://localhost:8080and the dashboard on its configured port. These endpoints are part of the learning preview, which is not on the invite-only hosted gateway (https://api.ultrarouter.io) yet — run the local stack or point at your own deployment. - A key and base URL. Set them once:
export UR_BASE_URL="http://localhost:8080"
export UR_API_KEY="ur_sk_live_…" # ur_sk_live_* or ur_sk_test_*- The
config:readscope on that key to read the plan projection over the API. Confirming a workflow is a signed-in action — it needs a session and theflywheel.writerole, never an API key — but it is not UI-only: the dashboard andPOST /v1/flywheel/plan/:id/confirmshare the one confirm assembly, so an SDK/agent with a signed-in session can confirm over HTTP too (a proposed objective never teaches, so the mint stays a human step). - Some real traffic. Discovery needs your own calls to read. Point an app you already run at the base URL above, or replay a slice of history — a handful of call types across a few dozen requests is enough to reach the structural a-ha.
1. Connect your traffic
There is nothing to configure. Change the base URL and key in the app you already have, and ultrarouter begins reading your calls in audit mode — it serves every request unchanged and learns the shape of your traffic as a side effect.
A 200 from key introspection proves your base URL and key reach the gateway:
curl -sS --fail "$UR_BASE_URL/v1/key" \
-H "Authorization: Bearer $UR_API_KEY" > /dev/null
echo "ok: gateway reachable — your traffic is now being read in audit mode"2. Read the structural a-ha
Open the Learn onboarding in the dashboard. Shortly after your traffic starts flowing it shows a picture of your account assembled from your requests alone:
- your call types ranked by the share of spend each accounts for, each with a request count and an archetype hypothesis (a prior, shown as such);
- contract-derived evaluator drafts — editable starting points for how each call type could be checked;
- mechanical and next-turn defect counts, each with its
n; - the outcome channels detected in your traffic, each with its rung on the
possible → configured → observed → qualifiedladder.
This step is diagnostic. It tells you what you are running and what could be measured — it never claims a workflow or an improvement.
3. Read the semantic a-ha as a proposed plan
Once enough signal accrues past the noise gate, ultrarouter proposes one workflow worth learning and assembles its plan. You can read that plan projection for a workflow over the API. It returns metadata only — ids, hashes, counts and the derived program — never a prompt or a body:
# Read the proposed learning plan for a workflow (needs a key with `config:read`).
curl -sS "$UR_BASE_URL/v1/flywheel/plan?workflow=$WORKFLOW_ID" \
-H "Authorization: Bearer $UR_API_KEY"
# While still collecting → { "object": "learning_plan_envelope", "workflow_id": "...", "plan": null }
# Once a proposal exists → plan.review_state: "proposed", with call_type_bindings and
# per-field provenance. A proposed plan carries NO artifact_type_version_id and NO
# spec_hash — because a proposal never teaches (see below).Expected state. While the workflow is still collecting, plan is null — an
honest state, never a faked plan. Once a proposal exists, plan.review_state is
"proposed" and the plan carries its call_type_bindings and per-field provenance,
but its artifact_type_version_id and spec_hash are empty: a proposal has no
compiled program, because it teaches nothing until you confirm it.
The dashboard renders the same proposal richly: the recommended workflow, at most two
alternative objectives, "why we suggested this", an estimate of resolved outcomes
per week with its n and interval, the channel and any missing source, provenance
chips (From your traffic / Drafted / Yours), and the one-sentence preview
Store X when Y happens; learn Z; apply it to calls W.
4. Confirm one workflow
Confirming is the whole job — and it is deliberately a signed-in dashboard action,
because it is the boundary where a draft becomes something the product may act on.
Review the proposal, edit any field (or describe the objective in your own words,
within the shown per-day and length limits), and Confirm. Each verb — Confirm, Edit,
Merge, Split, Decline (with a coded reason) — is recorded as product telemetry and
audited. Under the hood the dashboard records the verb through
POST /v1/flywheel/plan/:id/review and, on Confirm, calls the audited confirm service
that writes the confirmed objective version in one transaction — the SAME service
POST /v1/flywheel/plan/:id/confirm exposes, so the confirm is reachable over HTTP with
a signed-in session (never an API key).
What confirming does — and the law it obeys. Confirming mints a confirmed plan, the only input the trigger compiler accepts. This is a type boundary, not a runtime toggle: a drafted (LLM-suggested) value is not assignable into a confirmed plan, and a proposed objective reaching the compiler is a compile error. The confirm service reads the values you authorised, never the draft's proposed values — so editing a suggestion changes what you review but can never change what a confirmed plan teaches. This is why a proposed objective never teaches: there is no path from a proposal to a lesson that skips your confirmation.
Naming the label field (label_field). For a workflow whose "right answer" is a
field in a structured response, you can confirm which field is the label the
reference-agreement objective grades. Discovery proposes it for you: the plan review
(GET /v1/flywheel/plan, and the discovery card in the dashboard) carries a
label_field_proposal for each bound call type — its path, its basis
(corrections · convention · none) and a changed/total count per candidate
field. The proposal is structural, never an LLM guess — among your contract's scalar
leaf fields it picks the one your declared corrections change most often (edited finals
vs what was served), tie-broken to a field literally named label. It is
consent-gated: the change signal is derived only under corrections consent (basis: corrections); without consent no correction body is read and it degrades to the
convention/none basis. You then confirm it: pass label_field (a dot-path such as
intent or outcome.label) on the confirm request, accept the proposed default in the
card, or edit it. Naming it is what makes evaluation plug-and-play when your label field
is called intent, category or decision rather than label: an incidental field the
objective does not name (a notes a model rewrites each time) then never counts against a
right answer. It is a confirmed plan field like any other — only a value you confirm
enters the plan, never a bare proposal. The proposal is recomputed only while the plan is
unconfirmed (the review moment it serves); once you confirm, GET /v1/flywheel/plan
returns the confirmed field (label_field + its origin, label_field_proposal: null) and
reads no correction body — so a dashboard poll of a confirmed plan is free of audit noise.
After you confirm, the Flywheel card moves from proposal_ready to
confirmed_no_join (until the outcome source is wired) or resolving (once real
outcomes arrive). Every state is derived from the ledger — never a writable flag — so
it can only reflect what is actually true.
What this proves — and what it does not
Proves. You can go from raw traffic to a single, inspectable, confirmed workflow without a configuration form: connect, read the two a-ha payloads, read the proposed plan over the API, and confirm what "it worked" means — with every drafted value chipped as a suggestion and nothing acting on your requests until you confirm.
Does not prove. A confirmed workflow is a starting line, not a result. It does
not claim any improvement to your app: confirming an objective turns learning on
for that workflow, but a lift is only ever claimed later, behind a holdout, with its
scope, interval and n stated. Turning a confirmed objective into captured
corrections, a tested change and a live-confirmed comparison is covered by the
correction quickstart and the
application-CI and live-confirmation quickstart, each
published as it ships.
Next steps
- Read the Discovery concept for the two a-ha moments, the channel ladder, and the "proposed never teaches" law in full.
- Register what "it worked" means for the workflow you confirmed.
- Capture your first reviewer correction against it.
Plan a model migration
A bounded M0 walkthrough — from a model being deprecated to a scoped migration decision, with honest states for everything not yet built.
Capture your first reviewer correction
A bounded M1 walkthrough — send a request, join a human reviewer's correction to the original output in the ledger, and repair a failed import — with honest boundaries for what is captured now and what learning is still M2.