The full set of guides for self-hosting Winnow, integrating it with your stack, and running the hosted SaaS. Sourced directly from the repo's docs/ folder — what you see here is what ships in the latest release.
A pattern for building a persistent, evolving Product Intelligence system using LLMs.
This document defines a Product‑focused adaptation of the LLM Wiki pattern. Instead of transient retrieval or summarisation, an LLM incrementally builds and maintains a Product Intelligence Knowledge Base (PIKB) that compounds insight over time and supports evidence‑based product strategy and ideation.
The PIKB is designed to:
The LLM maintains the system. Humans provide judgement, strategy, and direction.
The PIKB consists of three layers:
A curated collection of unmodified source data:
Raw sources are never edited or summarised in place.
A directory of Markdown files generated and continuously maintained by the LLM. This layer contains:
The wiki represents the current best understanding of the product reality and evolves as new evidence is ingested.
A configuration document (e.g. AGENTS.md or CLAUDE.md) that defines:
This schema transforms a general LLM into a disciplined Product Intelligence system.
Before ingesting insight data, the wiki must be seeded with baseline product reality. These foundations anchor interpretation and prevent unconstrained ideation.
product_proposition.mdvision_and_strategy.mdlifecycle_model.mdtarget_markets.mdconstraints_and_assumptions.mdThese documents are treated as authoritative context and must be explicitly referenced when evaluating insights, opportunities, and ideas.
wiki/
index.md
log.md
foundations/
product_proposition.md
vision_and_strategy.md
lifecycle_model.md
target_markets.md
constraints_and_assumptions.md
customers/
personas/
segments/
jobs_to_be_done/
unmet_needs/
problems/
problem_statements/
pain_points/
insights/
customer_insights.md
sales_insights.md
usage_insights.md
market_insights.md
competitor_insights.md
competitors/
competitor_profiles/
comparison_tables/
opportunities/
opportunity_backlog.md
validated_opportunities/
discarded_opportunities/
experiments/
hypotheses.md
tests.md
outcomes.md
recommendations/
product_ideas.md
enhancement_candidates.md
When new raw sources are added, the LLM:
Insights are never isolated; they are always integrated into the broader product understanding.
Users query the wiki rather than raw data. Example questions:
High‑value analyses generated during queries are written back into the wiki so that learning compounds.
Periodic maintenance passes identify:
This keeps the PIKB current and decision‑ready.
Ideas are not generated directly from insights. Instead, the LLM is trained to identify opportunities by detecting tension between:
Opportunities are structured artifacts, each linked to supporting evidence and strategy.
Product ideas and enhancements are proposed only when:
Each idea includes:
Low‑confidence ideas are flagged rather than obscured.
index.mdA structured catalogue of all wiki content with short summaries. Used by the LLM for navigation and synthesis.
log.mdAn append‑only chronological record of ingestions, analyses, belief changes, and lint passes. This provides historical context for decision‑making.
Product knowledge systems fail when maintenance effort exceeds value. In the PIKB model:
The LLM handles maintenance and synthesis. Humans focus on judgement, prioritisation, and leadership.
This is a pattern, not a product.
When implemented with clear schema rules and strong strategic anchors, a Product Intelligence Knowledge Base becomes:
The LLM maintains the system. Product leaders do the thinking.
A plain-English walkthrough of using Winnow from the command line. This guide assumes you've already installed it (see the README's Install section) and are looking for the "now what?" answer.
If you'd rather click around in a browser than type commands, jump to the Web UI guide. Both surfaces talk to the same project — switch freely between them.
If Python isn't your thing, the
Docker walkthrough gets you running with a
single docker run — same CLI, same Web UI, no Python install on
the host. The container also dispatches CLI commands directly:
docker run --rm winnow:latest winnow status works out of the box.
Winnow is a Product Intelligence Knowledge Base. You feed it raw
evidence (sales call notes, support tickets, research transcripts,
competitor intel), and it maintains a curated wiki of insights,
problems, opportunities, and recommendations on top. The wiki is
edited by the LLM, never by hand. You stay in the loop by reviewing
proposed changes (--dry-run), inspecting the diff, and committing
the ones you want to keep.
The whole project lives in a single directory:
raw/ — every piece of evidence you've ever fed in. Append-only.wiki/ — the LLM-maintained synthesis. Insights, customers,
opportunities, recommendations — and a small set of foundations
that anchor everything (your strategy, target markets, lifecycle,
product proposition, constraints)..winnow/ — bookkeeping (state, log, proposals). Mostly leave
this alone.mkdir my-product-intel && cd my-product-intel
winnow init
This creates the raw/, wiki/, .winnow/ directories and writes
five foundation stub files under wiki/foundations/. These are
the most important files in your project. They define what your
product is, who it's for, what it isn't, and what constraints
shape every decision.
Open each foundation file and replace the TODO content with the real thing. There are five:
product_proposition.md — what you sell, to whom, for what
outcomevision_and_strategy.md — where you're heading and howtarget_markets.md — who counts as a customer; who doesn'tlifecycle_model.md — the stages a customer moves throughconstraints_and_assumptions.md — the things you're treating as
fixedDon't overthink these. A first draft is enough — the LLM will flag things that don't fit, and you'll refine them via the reconcile flow (more on that below) as decisions and delivery play out.
Configure Winnow:
winnow setup
The walkthrough prompts for your Anthropic / OpenRouter API key,
synthesis + selector models, and offers to generate a bearer
token (used by the webhook ingestion API, the MCP HTTP transport,
and winnow gateway). Pressing Enter accepts the default shown
in [brackets]. You can re-run winnow setup any time to rotate
or update values.
You're ready.
The non-interactive path. Already have keys in
ANTHROPIC_API_KEY/OPENROUTER_API_KEYenv vars? Skip the walkthrough —winnow setup --non-interactivereads them directly into the SQLite config store at.winnow/config.db. This is also what the Docker image's entry-point runs on first boot; seedocs/docker-deployment.md.
The most common loop is short:
Drop new evidence into raw/. Sub-folders are
sales/ · customer_success/ · support/ · research/ ·
market/ · competitors/ · systems/ · decisions/ ·
delivery/. Supported formats: .md, .txt, .pdf, .docx.
Plain text needs no frontmatter or structure — just the evidence.
PDFs and Word docs are converted to text in-memory at ingest
time (the original binary stays on disk). Four equivalent input
paths: drag-and-drop in your editor, the Create evidence
button on /raw in the Web UI (WYSIWYG
markdown or PDF / DOCX upload),
Webhook ingestion (n8n / Zapier / Make,
text only), or Cloud-sync ingestion
(Dropbox / iCloud / Google Drive / OneDrive). Pick whichever
fits the moment — winnow ingest treats them all the same.
See what Winnow thinks.
winnow ingest --dry-run
This reads the new evidence, decides what wiki pages should change, and prints the plan without writing anything.
Run for real.
winnow ingest
Same plan, this time applied to wiki/.
Review and commit.
git diff wiki/
git add wiki/ && git commit -m "ingest: <what changed>"
That's it. Repeat as new evidence comes in.
winnow ingest is the general path. There are two narrower commands
for specific kinds of evidence:
winnow decide — for decisions. Reads only raw/decisions/.
Writes to wiki/insights/decisions/.winnow deliver — for delivery outcomes. Reads only
raw/delivery/. Writes to wiki/insights/delivery/.Use them when you're explicitly logging "we decided X" or "we
shipped Y" — the prompts are tuned for those flows. Otherwise,
ingest covers the rest.
Once you've ingested enough evidence to have a few insights, run:
winnow discover --dry-run
winnow discover
This scans the wiki for tensions — recurring pains, contradictions,
gaps — and proposes opportunities. Each opportunity has cited
evidence. You can scope a run with --limit N if you want a slower
introduction.
Discover is opportunity-first by design: you don't go from a single piece of evidence straight to "let's build a feature." The opportunity is the unit that earns a recommendation.
Once you've validated an opportunity (in your head, in a meeting, or by deciding to invest in it), run:
winnow recommend --opportunity opportunities/enterprise-sso-gap
Or run it without --opportunity to let Winnow pick one for you.
Recommendations carry an explicit confidence, risks, and alignment with strategy. They're proposals, not facts. You can ignore them, refine them, or build them.
The five foundation files are constraints, not suggestions. They shape every interpretation Winnow makes. So they only change for two reasons:
raw/decisions/).raw/delivery/).Run:
winnow reconcile --dry-run
winnow reconcile # writes a proposal — does NOT touch foundations yet
Reconcile is deliberately a two-step workflow: it proposes
changes to your foundation files and saves them as a sidecar in
.winnow/proposals/<id>.{json,md}. It does not modify the wiki.
Then you review:
winnow reconcile --list # see pending proposals
Open the .md file, read the proposed changes. If they look right:
winnow reconcile --apply <id>
If not:
winnow reconcile --discard <id>
This guardrail exists because foundations propagate. Updating them without explicit human review would let one weird Sunday-afternoon LLM call drift your strategy.
winnow lint
Static checks — no LLM, no API spend. Catches:
(conflicted copy)
files lingering in raw/)Run it any time. Run it before commits. CI it if you want.
For the smarter version that costs API tokens:
winnow lint --deep
This adds an LLM audit that surfaces things only a human-or-LLM could see — contradictions across pages, stale insights overtaken by newer evidence, weak connections. Useful occasionally, not every commit.
winnow status # snapshot of project state
winnow log -n 20 # last 20 operations and what they touched
winnow reindex # regenerate wiki/index.md (Winnow does this for you, but you can force it)
Runtime config (LLM API keys, bearer tokens, model picks, rate
limits) lives in a SQLite store at .winnow/config.db (Inc 27).
Three places mutate it; pick whichever fits:
# Interactive walkthrough — defaults shown in [brackets].
# Re-run any time to rotate keys / regenerate tokens.
winnow setup
# Per-key CRUD from the terminal — JSON-decoded so `4` lands as
# int, `true` as bool, plain strings round-trip. `list` redacts
# anything ending `_api_key` plus `api_tokens`.
winnow config get selector_model
winnow config set selector_model anthropic:claude-haiku-4-5
winnow config set max_concurrency 4
winnow config set api_tokens '["tok-a","tok-b"]'
winnow config list
winnow config unset selector_model
# Browser surface — the Web UI's `/settings` page (Inc 27d).
# Tabbed into Models / LLM provider keys / Bearer tokens /
# Operational. Same DB; live changes apply without a restart.
winnow ui
The DB is the source of truth at runtime. Env vars
(ANTHROPIC_API_KEY, OPENROUTER_API_KEY, WINNOW_API_TOKENS,
etc.) are read on the first Config.load() call after a
project is created — values get migrated into the DB and the
env vars are dormant thereafter. Two exceptions stay live:
WINNOW_TRUSTED_PROXY and WINNOW_PROJECT_ROOT are read at
every boot because they shape how the backend boots, not what
config it loads.
winnow mcp # start the MCP server on stdio (blocks until stdin closes)
winnow mcp exposes 7 read-only tools — list_recommendations,
get_recommendation, list_opportunities, get_opportunity,
get_wiki_page, get_foundations, list_recent_changes. AI
builders (Claude Desktop, Cursor, Claude Code, etc.) spawn this as
a subprocess and call the tools when they need wiki context.
You normally don't run winnow mcp by hand — your AI builder runs
it for you, configured via a JSON config file. The Web UI's
Integrations page (/integrations) generates the config
snippet for the major builders with the project root pre-filled,
so you can paste verbatim.
For remote use (hosted SaaS, AI builder on a different machine),
winnow ui exposes the same tools over HTTP at
http://localhost:8000/api/mcp with bearer auth — see the
README's MCP server section.
Open WebUI, Continue.dev, LibreChat, LobeChat — any client that speaks the OpenAI API can chat with Winnow:
winnow gateway
# Starting Winnow gateway at http://127.0.0.1:11434 …
The gateway exposes POST /v1/chat/completions and
GET /v1/models at port 11434 (Ollama's default port —
Open WebUI auto-discovers it). Each chat turn loads foundations
Configure your client's OpenAI base URL and API key:
http://localhost:11434/v1winnow setup / the
Web UI's /settings page (Inc 27)winnow appears as a model in the picker. Bearer auth gates
everything; with no tokens configured in the DB, requests get 503
with a clear "tokens not configured" message. The gateway loads
its config once at startup, so restart it after generating a new
token.
For the CLI flag reference and the gateway's exposed routes,
see the README's winnow gateway section. For a full Open
WebUI walkthrough — Docker setup, docker-compose, Continue.dev
/ LibreChat / LobeChat / Cursor configurations, and
troubleshooting — see
docs/open-webui-integration.md.
winnow ingest / discover / recommend / reconcile runs can
fire HTTP POSTs to subscribed URLs (n8n / Zapier / Slack / a
custom service) on completion. Configure subscribers via the CLI
(a dedicated editor on /settings is a deferred follow-on):
winnow config set outbound_subscriptions '[
{
"url": "https://n8n.example.com/webhook/winnow",
"events": ["log.appended", "proposal.created"],
"secret": "your-shared-secret"
}
]'
Four event types: log.appended (every successful pipeline run),
proposal.created (reconcile-propose lands a sidecar),
proposal.applied (proposal applied via the Web UI's
/proposals/[id] page), proposal.discarded (proposal discarded).
Delivery is fire-and-forget — the pipeline doesn't wait. See the
README's Outbound webhooks section for the envelope shape, HMAC
verification snippet, and retry semantics. Diagnose delivery
problems on the Integrations page (/integrations) in the Web UI.
Always start with --dry-run. It's free (well, it costs the
same in tokens, but nothing is written). The diff is your quality
gate.
Commit small. One ingest, one diff, one commit. If a run produces fifty changes you wouldn't want as one commit, split the raw evidence and run separately.
Foundations are sacred. Don't edit them directly to "fix" a
reconcile proposal — instead, discard the proposal, fix the
underlying decision/delivery doc in raw/, and re-run.
The log is append-only. Every operation writes to
wiki/log.md — that's how you reconstruct what changed and why.
Don't manually edit log.md; Winnow uses it for state.
Cost control. Each ingest, discover, recommend,
reconcile, and lint --deep makes LLM calls. --dry-run and
the deep-lint runs cost the same as the real thing. Static
lint, log, status, and reindex cost nothing. Add a
pricing map (winnow config set pricing '...') to see per-run USD estimates
in the Web UI (see "Cost estimation" in the README's
Configuration section); the CLI doesn't render dollars itself,
but token totals show up in the operation result panels and
on the dashboard's "Last operation" card.
Provider override. Pass --model anthropic:claude-sonnet-4-6
(or any other prefixed model) on any LLM-driven command if you
want a different / cheaper / faster model than your stored
default.
Conflict files from cloud sync. If you're feeding raw/
through Dropbox / iCloud / Google Drive / OneDrive, sync
conflicts produce extra files (winloss (conflicted copy …).md).
winnow ingest auto-skips Dropbox's distinctive pattern with a
warning; winnow lint flags it (and Google Drive's (n)
pattern) so you remember to clean them up. iCloud's bare 2
/ 3 and OneDrive's device-suffix style are too generic to
filter automatically — handle by hand.
raw/ automatically, see Webhook ingestion.raw/ via Dropbox / iCloud / Google
Drive / OneDrive sync, see Cloud-sync ingestion.A plain-English walkthrough of using Winnow in the browser. The web UI sits on top of the same project as the CLI — you can use either or both, switch freely, and they always see the same data.
If you'd rather work from the terminal, see the CLI guide. The two share a mental model; the web UI just makes browsing and reviewing nicer.
Use the browser when you want to:
discover or reconcile run actually produced
— proposals especially benefit from a side-by-side review pane
with apply/discard buttons.The CLI is still the right call when you're scripting, doing many things back-to-back, or want a clean git diff before committing.
winnow ui
That's it. One command boots the backend, the frontend, waits for both to come online, and opens your browser.
Useful flags:
--dev — use Next.js's hot-reload dev server instead of the
production build. Slower first paint, faster subsequent edits.--no-frontend — start only the backend (useful if you're
running npm run dev in another terminal yourself).--no-browser — start everything but don't open the browser
(useful if you're sharing your screen and don't want the tab to
pop).Ctrl-C in the terminal stops both processes cleanly.
If winnow ui reports errors about missing node_modules or
.next/, follow the install steps in the
README's Web UI section — you need to run
npm install && npm run build once before the production launcher
will work.
/)A snapshot of the project: which directory you're in (top of the header), how many wiki pages exist by category, the raw worklist state (how many sources are new, revised, or unchanged since you last ingested), and the latest log entry.
Useful as a homepage; not where you actually do work.
/wiki)Browse every wiki page grouped by category — foundations,
customers, problems, insights, opportunities, recommendations, etc.
Clicking a page opens a detail view with the rendered markdown, the
frontmatter sidebar (so you can see citations and links), and a
back-references panel showing every other page that links to
this one (via related:, evidence:, or opportunity: fields).
The wiki is read-only in the UI — pages get edited by Winnow's operations, never by hand.
When you open a recommendation page (e.g.
/wiki/recommendations/ship-saml-sso), an Export to prompt
button appears next to the type badge. Clicking it opens a side
panel that assembles a paste-ready brief from the recommendation,
its parent opportunity, the evidence the opportunity cites, and
the five foundation pages — labelled as non-negotiable
constraints so an AI builder treats them as guardrails.
The panel shows a metadata header (char count, included page count, any broken links) and a scrollable preview. Copy to clipboard drops the markdown into your buffer; paste it into Claude Code, Cursor, Lovable, or any other AI builder that takes markdown context. No LLM tokens are spent on the assembly — it's pure templating, instant and deterministic.
If a referenced page is missing (broken evidence link, missing foundation file), the panel surfaces it as a "missing" badge in the metadata header. The brief still copies cleanly with whatever context is available, so partial graphs don't block the export.
/raw)Every piece of evidence under raw/, grouped by source type. Each
row carries a status badge:
ingest.A Create evidence button in the toolbar opens a dialog with two tabs:
manual-ui..pdf and .docx (max 25 MB). The
binary is preserved in raw/ untouched; the LLM reads extracted
text at ingest time via pypdf / python-docx. Source defaults to
manual-upload. Scanned PDFs need OCR before upload — Winnow
won't OCR for you (lint flags zero-extraction binaries so you
spot them).Both tabs share the rest of the form: subdir, optional filename, optional source / external id overrides, and an Auto-ingest after creating checkbox that runs the synthesis pipeline once the debouncer's quiet window settles — useful when you want the LLM to chew on what you just dropped in without switching to the Operations page.
This is the fourth way to add evidence — alongside dropping a file
in your editor, the webhook API, and
cloud-sync ingestion. All four converge
on "a file lands in raw/<subdir>/" — pick whichever fits the
moment.
Whenever any receipts have landed via any input path, a small
Recent receipts card appears at the top of /raw. Each row
shows the subdir, source label (manual-ui, hubspot, n8n, etc.
— makes the input path obvious at a glance), filename, external id
if any, and timestamp. If the receipt asked Winnow to auto-ingest,
the row also carries a status badge — scheduled (debouncer hasn't
fired yet), pending / running (job in flight), done, or
failed. Receipts that didn't request auto-ingest render no badge.
Cloud-sync providers can leave conflict files when the same
file is edited on two devices. The lint page flags those as
conflict-file-litter so you can clean them up.
Click any row to read the source. Sources are immutable — you can read but not edit. The Create form only writes new files; it never modifies existing ones.
/log)Every operation Winnow has ever run, append-only, newest-first. Filter by mode (ingest / discover / reconcile / etc.) and expand a row to see exactly which sources fed in, which pages were created/updated, and any belief changes that were noted.
This is your audit trail. It's also append-only on disk —
wiki/log.md is the canonical version; /log just renders it.
/operations)Where you run the LLM-driven commands. Six tabs across the top, one per operation:
raw/decisions/ only)raw/delivery/ only)Each tab has its own form. Common pattern:
pricing map in the
config DB (winnow config set pricing '...'), the card title
gains "& cost", per-phase rows show the model + USD cost, the
total row shows the run total, and the dashboard's "Last
operation" card surfaces the same USD figure for the most
recent run. Without it, the cost columns simply don't render
— see the README's "Cost estimation" section to set rates up.The page mirrors the active operation in the URL
(?job=<id>&mode=…), so you can refresh during a long run and pick
up where you left off, or share the link with a colleague who's
also running this Winnow instance.
For Reconcile specifically, the result panel ends with a "Review proposal" button that takes you straight to the proposal review page (next section).
/proposals and /proposals/[id])When reconcile produces a proposal, it lands here as a pending
sidecar. The list view shows each one with its summary, edit count,
and which decision/delivery sources triggered it. Clicking through
opens the review page:
Both buttons open a confirmation dialog before doing anything. The
apply dialog reminds you to check git diff wiki/ after — Winnow
doesn't auto-commit the changes.
This is the only place foundations get edited. Direct edits in your text editor still work, but they bypass the audit trail.
/lint)The static health checks load on page open: foundation completeness, frontmatter shape, broken links, opportunities without evidence, etc. Pass/fail banner at the top, findings grouped by severity and code below.
Below that is a Run deep lint button. This costs API tokens — it asks the LLM to read every non-foundation page and look for contradictions, stale insights, unframed pains, and so on. Findings are advisory — they don't change the wiki, just surface things worth thinking about. The button stays available so you can re-run without leaving the page.
/integrations)The Integrations page is where you wire AI builders (Claude Desktop, Cursor, Claude Code, Lovable, anything else that speaks MCP) to Winnow's wiki, plus the OpenAI-compatible gateway for Open WebUI / Continue.dev / etc., plus outbound webhooks. The MCP-server card has four sections:
list_recommendations, get_recommendation, etc.)
with one-line descriptions. The list is driven by the live
server so it never drifts from what your AI builder will see.<your-bearer-token> placeholder — copy the
token from /settings → Bearer tokens → Generate token (the
plaintext is shown once; copy it then). Helper text under each
tab tells you which file to paste the snippet into.winnow ui
wipes it.The MCP server is read-only by design — AI builders can pull wiki
context but can't add evidence or edit pages. To add evidence from
an AI builder session, paste the relevant content into the
Create-evidence form on /raw.
A separate panel on the Integrations page describes the
winnow gateway command — an OpenAI-compatible chat-completions
service that lets Open WebUI / Continue.dev / LibreChat / Cursor
"chat with Winnow" via the OpenAI API. The card shows the
default base URL (http://127.0.0.1:11434/v1 — port matches
Ollama for auto-discovery), the start command (winnow gateway),
and a pointer at the full integration walkthrough
(docs/open-webui-integration.md). The gateway reads its bearer
tokens from the same config DB the rest of Winnow uses — no env
var needed.
The gateway runs as a separate process from winnow ui, so the
panel is informational only — there's no live status query
because the gateway lives at a different port and may or may
not be running. Walkthrough steps for connecting Open WebUI in
particular (Docker setup, model picker config, troubleshooting)
live in the dedicated doc.
A second card on the same page covers outbound delivery — Winnow notifying external systems (n8n / Zapier / Slack / a custom service) when a pipeline event fires. The card shows:
outbound_subscriptions config row (winnow config set outbound_subscriptions '[…]' from the CLI; a dedicated
editor on /settings is a deferred follow-on), with the
events it's subscribed to and a signed / unsigned badge
(signed means a HMAC-SHA256 secret is configured; the secret
value never reaches the UI).When no subscribers are configured, the card surfaces a brief
explainer pointing at the four event types and the secret
opt-in. Useful for diagnosing "why isn't my n8n flow getting
events?" without tailing stderr — fire a fresh ingest run, watch
the row land in the feed (or fail loudly enough to spot the
problem).
The dashboard (/) carries a small status card linking to this
page so you can tell at a glance whether the integration surface
is enabled.
A first-time week might look like this:
Monday morning: dropped a few sales call notes into
raw/sales/ over the weekend. Open /operations, hit the
Ingest tab → Dry run, watch the plan. Hit Run for
real, watch the live log, see the new insights land. Pop to
/wiki to skim them.
Wednesday: there's enough material to find patterns. Open
/operations → Discover → Run for real. Let it propose
five opportunities. Open /wiki/opportunities/ to read them.
Friday: a leadership decision came in — "we're going
upmarket, mid-market is no longer a focus." Drop a markdown
note into raw/decisions/upmarket-pivot.md. Open
/operations → Decide → run. Then Reconcile → run
(this proposes baseline changes, not changes the wiki). Open
/proposals, click the new proposal, review the proposed
updates to target_markets.md, hit Apply.
You'd run lint occasionally, browse /log if you wanted to see
what an op did last month, and check /raw whenever you wanted to
know what was queued for ingestion.
Dry-run first is always cheap insurance. The diff is your quality gate before anything writes.
The header shows your project root, truncated. Hover for the
full path. Winnow finds the project from your current directory
when you ran winnow ui, not from the URL — make sure you're in
the right place when you start.
One backend = one project. If you want to look at two
Winnow projects in two browser tabs, run two winnow ui
invocations on different ports (winnow ui --port 8001,
winnow ui --port 8002). Each opens its own browser tab.
You can't edit pages in the UI. That's intentional —
edits-by-hand bypass the LLM's reasoning and the audit trail.
Drop new evidence into raw/ instead, or accept/discard
proposals.
The web UI is local-only by default. It binds to
127.0.0.1. Nobody else on your network can see it unless you
explicitly run winnow ui --host 0.0.0.0 (and even then, you
should set up bearer tokens if you do — see the
README).
All commits are still your responsibility. Winnow writes
files; git is yours. Run git diff wiki/ after each operation
and commit when you're satisfied.
raw/ via cloud sync, see
Cloud-sync ingestion.Worked examples for pushing raw evidence into Winnow from the major workflow platforms. Quick reference for the API contract is in the README's Webhook ingestion section.
One-off entry? If you just want to type or paste a single piece of evidence by hand, the Create evidence button on
/rawin the Web UI is faster than wiring a webhook. The recipes below are for automation — recurring pushes from external systems. See the web user guide for the UI form.
All recipes assume:
http://127.0.0.1:8000. If
you're running Winnow on a different host, swap the URL and bear
in mind it's still bound to 127.0.0.1 by default — flip
--host 0.0.0.0 on winnow ui (or run uvicorn directly with
--host 0.0.0.0) to expose it.winnow setup (interactive) or the Web UI's /settings page →
Bearer tokens → Generate token (Inc 27). The Docker image
auto-generates one on first boot and prints it via docker logs.Every recipe boils down to a POST /api/raw/{subdir} with this
JSON body:
{
"content": "<markdown body — your formatted evidence>",
"filename": "<optional — auto-generated if absent>",
"source": "<your tool / system name>",
"external_id": "<stable upstream id, e.g. HubSpot deal id>",
"frontmatter": {
"<extra-key>": "<extra-value>"
}
}
Headers:
Authorization: Bearer <your-token>
Content-Type: application/json
Pick a subdir from the canonical taxonomy
(sales, customer_success, support, research, market,
competitors, systems, decisions, delivery).
Optional query params:
?auto_ingest=true — debounce-and-fire winnow ingest after the
configured quiet window (default 30s). Bursts collapse to one
ingest run. The response gains auto_ingest_scheduled: true. See
the Auto-ingest section in the README
for the full contract.n8n's HTTP Request node is the only node you need.
content string.POSThttp://winnow-host:8000/api/raw/sales
(append ?auto_ingest=true to have Winnow run ingest
automatically after the burst settles).Authorization and value
Bearer your-token-here.JSON{
"content": "{{ $json.markdown_body }}",
"filename": "winloss-{{ $json.deal_id }}.md",
"source": "hubspot",
"external_id": "{{ $json.deal_id }}"
}
content returns 409 with the
existing path. If you want n8n to treat that as success, add an
On Error branch that ignores 409.content string in a Function
node so you have full JS access to the upstream payload. Use
template literals to assemble the markdown:
const deal = $input.first().json;
return [{
json: {
markdown_body: `# Lost ${deal.dealname}\n\n` +
`**Stage:** ${deal.dealstage}\n` +
`**Reason:** ${deal.closed_lost_reason}\n\n` +
`${deal.notes_last_updated}\n`,
}
}];
subdir into the
URL via an n8n expression: /api/raw/{{ $json.subdir }}.true and inspect the response code in a downstream
branch. 201 = new, 409 = dupe, anything else = real error.Use the Webhooks by Zapier — POST action.
content from the trigger payload. Output a single object with
markdown_body, external_id, etc.POSThttp://winnow-host:8000/api/raw/salesfalseAuthorization: Bearer your-token-hereContent-Type: application/json{
"content": "{{markdown_body}}",
"filename": "winloss-{{deal_id}}.md",
"source": "hubspot",
"external_id": "{{deal_id}}"
}
markdown_body
field. Newlines survive Zapier's templating.Use the HTTP — Make a Request module.
http://winnow-host:8000/api/raw/salesPOSTAuthorization, Value Bearer your-token-hereContent-Type, Value application/jsonRawJSON (application/json){
"content": "{{1.markdown_body}}",
"filename": "winloss-{{1.deal_id}}.md",
"source": "hubspot",
"external_id": "{{1.deal_id}}"
}
Yes (so 4xx / 5xx pop into the Make error
handler).content must be a string).bundle.statusCode to branch on 201 (new), 409
(dupe), or other (error).For ad-hoc scripts (e.g. a cron job that pulls competitor release notes, scrapes pricing pages, mirrors a Discourse forum):
import datetime as dt
import requests
WINNOW_URL = "http://127.0.0.1:8000"
TOKEN = "your-shared-secret-here"
def push_raw(subdir: str, content: str, *, source: str, external_id: str,
filename: str | None = None) -> requests.Response:
payload = {
"content": content,
"source": source,
"external_id": external_id,
}
if filename:
payload["filename"] = filename
resp = requests.post(
f"{WINNOW_URL}/api/raw/{subdir}",
json=payload,
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
if resp.status_code == 409:
# Idempotent retry — content already on disk. Treat as success.
return resp
resp.raise_for_status()
return resp
# Example: mirror a competitor's release notes RSS into raw/competitors/
for entry in fetch_release_notes("https://example.com/releases.xml"):
push_raw(
"competitors",
content=f"# {entry.title}\n\n{entry.body}\n",
source="competitor-rss",
external_id=entry.id,
)
The 409-as-success handling lets the cron job re-run end-to-end without producing duplicates — every entry hashes the same on each poll, and the second poll onwards is a no-op for unchanged items.
| Symptom | Likely cause |
|---|---|
503 Service Unavailable with "no tokens configured" | No bearer tokens stored in .winnow/config.db. Run winnow setup (interactive) or hit the Web UI's /settings page → Bearer tokens → Generate token. Live changes apply without restart (Inc 27). |
401 Unauthorized | Header missing, malformed (Authorization: Bearer <token>), or token doesn't match any configured value. Check for trailing whitespace in the token. |
404 Not Found on POST /api/raw/<subdir> | Subdir isn't in the taxonomy. See the canonical list above. |
400 Bad Request with "filename must match…" | Caller-supplied filename failed validation (slashes, leading dot, bad extension, special chars). Drop the filename field to let Winnow auto-generate one. |
413 Payload Too Large | Body exceeds 1 MB. Trim or split. |
409 Conflict with path in the response | Idempotent retry — same body hash already on disk. Treat as success. |
409 Conflict with "different content" | Different content with the same filename. Pick a different name or omit filename to auto-generate. |
Webhook lands but winnow ingest doesn't see it | Check .winnow/state.json — ingest only re-processes new / revised sources. The webhook write is treated as a new source on the next ingest run. |
For the deferred items (HMAC verification, token-management UI,
outbound webhooks, provider-specific templates) see the backlog at
docs/implementation/to-do.md.
A second integration path alongside the
webhook API: point a cloud-sync folder
(Dropbox, iCloud, Google Drive, OneDrive) at Winnow's raw/
directory, and the existing winnow ingest flow Just Works.
No API tokens, no HTTP, no auth surface. Drop a file on any device, the sync provider gets it onto the box where Winnow runs, and the next ingest picks it up.
When to reach for this vs. the webhook API:
Use both if you like. They write to the same raw/ tree.
Symlink raw/ to a folder inside your sync provider.
cd my-product-intel
mv raw raw.local # if raw/ already exists
ln -s ~/Dropbox/winnow-raw raw # adjust the path for your provider
mv raw.local/* raw/ 2>/dev/null && rmdir raw.local
Now every file under raw/ is synced; wiki/ and .winnow/
(including the config.db SQLite store with your LLM keys
and bearer tokens) stay local to this machine.
Why this shape:
.winnow/state.json is updated on every op (it tracks which
raw files have been ingested at which hash). Keeping it out of
the sync means no cross-device merge conflicts on the
bookkeeping.wiki/ is LLM-maintained. Multi-device editing of the wiki
is a semantic problem — let one machine be the "ingest box" and
use git or a separate process to share the wiki if you need to.raw/ is append-only. Multiple devices writing to it is
fine; the conflicts cloud sync produces are limited to "two
people edited the same file at once," which is rare for raw
evidence (you don't usually edit it after dropping it in).If you'd rather have a strict separation between "stuff I drop in manually on this machine" and "stuff that comes from the cloud":
mkdir -p raw/inbox
ln -snf ~/Dropbox/winnow-inbox raw/inbox
raw/inbox/ becomes the synced bucket. The downside is mental
overhead — you have two places to look. The recommended pattern
above is simpler unless you have a specific reason to split.
The mechanics are nearly identical across providers. The differences are mostly:
ln -s ~/Dropbox/winnow-raw raw
Default sync location: ~/Dropbox/. Conflict files look like
<name> (Paul's conflicted copy 2026-04-29).md.
If you have Smart Sync / online-only files turned on, mark the
winnow-raw folder as "Make Available Offline" — Winnow needs to
read the actual file content, not the placeholder.
ln -s ~/Library/Mobile\ Documents/com~apple~CloudDocs/winnow-raw raw
Or, if you've enabled Desktop & Documents in iCloud, drop the
folder anywhere under ~/Documents/.
iCloud's conflict-file naming is not distinctive — it appends
2, 3 to the filename (e.g. winloss 2.md). That's too
generic to filter automatically; if you see them, rename or delete
by hand.
iCloud aggressively offloads files; mark the folder for "Always Keep on This Mac" via Finder so Winnow doesn't trip on placeholder files.
ln -s "/Users/$USER/Library/CloudStorage/GoogleDrive-<your-email>/My Drive/winnow-raw" raw
(Adjust the path for your Google Drive setup — locations vary between the official client and "Drive for Desktop.")
Google Drive uses (1), (2) for some auto-renamed conflicts
(e.g. winloss (1).md). Mostly rare.
Google Drive also creates .DS_Store and the like; Winnow
ignores anything that isn't .md or .txt, so they're harmless,
just visual noise.
ln -s ~/OneDrive/winnow-raw raw
OneDrive uses device-suffix conflict naming
(winloss-paul-mbp.md). Distinctive enough to spot at a glance
but not always automatic-filter-friendly.
OneDrive's "Files On-Demand" feature uses placeholder files. As
with the others, mark the winnow-raw folder for offline access
so Winnow can read it.
It's tempting to drop the entire my-product-intel/ directory into
your sync provider. Don't.
.winnow/state.json is rewritten on every operation. With cloud
sync, every device gets its own version, and conflict resolution
is undefined.wiki/log.md is append-only — but two devices appending in
parallel produces a merge conflict the cloud provider can't
resolve cleanly.wiki/index.md is regenerated on every ingest. Same pattern.wiki/'s contents are semantic, not file-level mergeable. Two
devices both running winnow ingest in parallel could write
contradictory updates to the same insight page.If you want multi-device workflows on the wiki, use git. Let one machine be the "ingest box," push wiki changes to a remote, pull on the other devices.
When two devices edit the same file at once, your sync provider produces a conflict file. Winnow handles them in two layers:
winnow ingest — silently skips the distinctive Dropbox
pattern ((conflicted copy)) so the LLM doesn't double-count
the same evidence. With --verbose (or via the live log on the
web UI's /operations page) the skip is logged as a warning so
you can see what was filtered.winnow lint — flags Dropbox conflict files and Google
Drive's (n) pattern (e.g. winloss (1).md) as
conflict-file-litter warnings. Lint surfaces both because the
litter shouldn't sit around in raw/ even if ingest can route
around the Dropbox ones.The patterns Winnow does not auto-detect:
2 / 3 suffix (e.g. winloss 2.md) — too
generic to filter without false positives. iCloud users: spot
them in Finder and rename or delete by hand.winloss-paul-mbp.md)
— also too generic. Same story: handle by hand. (n) pattern is matched by lint but not
auto-skipped on ingest, because the same shape can appear in
intentional human filenames (notes (1).md for v1, etc.).
Lint warns; ingest still picks the file up.The right resolution is almost always:
| Symptom | Likely cause |
|---|---|
winnow ingest reports zero new sources, but I just dropped a file | Sync isn't complete on this machine yet. Wait for the cloud-provider's status icon to go green. |
| Permission errors when reading files | Sync provider is using on-demand / placeholder files. Mark the folder for offline access. |
Files in raw/ look duplicated (winloss.md, winloss 2.md) | Either iCloud's auto-rename pattern or a manual copy. Resolve by hand — Winnow doesn't auto-filter the bare-number suffix. |
Two devices both ran winnow ingest and now wiki/ is a mess | Don't sync wiki/ — see the warning above. Recover via git: pick one device's wiki state and git checkout -- wiki/ on the other. |
winnow status shows the same source as "new" after every ingest | The file is being touched by sync (re-downloaded, re-permission'd) which changes its hash. Check whether your sync client is mangling line endings or metadata; consider moving the file outside the sync, ingesting, then moving it back. |
The post-v1 backlog at
docs/implementation/to-do.md tracks
related follow-ons that are explicitly excluded:
winnow watch command that auto-detects newly-synced files
and runs winnow ingest for you (cloud providers already raise
desktop notifications on sync; this would mostly be a small
ergonomics win).wiki/ itself (semantic conflicts; out of file-
level resolution's depth).Winnow's winnow gateway (Inc 25b–c) exposes an
OpenAI-compatible chat-completions API at localhost:11434 so
any OpenAI-shaped client — Open WebUI, Continue.dev, LibreChat,
LobeChat, Cursor's OpenAI-compatible mode — can chat with
Winnow as if it were a model. This page walks through the
Open WebUI setup end to end; the same pattern works for the
others.
A winnow entry in Open WebUI's model picker. Each chat turn:
wiki/foundations/* + a recency-capped sample of
wiki/insights/, wiki/opportunities/,
wiki/recommendations/ as the system prompt (built fresh on
every request — no manual context-pasting).synthesis_model (Anthropic /
OpenRouter — whatever the SQLite config store at
.winnow/config.db carries).Other modes (winnow/wiki Q&A, winnow/discover opportunity
discovery) lift via model-name suffix in a follow-on increment;
v1 is the recommendation flow.
pip install -e . from the repo, or
pip install winnow once published).winnow init) with at least the
foundation files filled in.winnow setup (interactive) or hit the Web UI's /settings
page → Bearer tokens → Generate token (Inc 27). The same
tokens gate the webhook + MCP HTTP surfaces, so if you've
already configured them for those, you're done.If no bearer tokens are configured, every gateway request returns
503 — tokens not configured. That's the fail-closed contract;
it's not a bug.
The simplest setup: gateway running on the host, Open WebUI in its own container talking to host networking.
winnow gateway
# Starting Winnow gateway at http://127.0.0.1:11434 …
# Point Open WebUI / any OpenAI-compatible client at
# http://127.0.0.1:11434/v1 (Bearer token from `winnow setup` /
# the `/settings` page).
Default port 11434 matches Ollama's default — Open WebUI
auto-discovers anything serving OpenAI-shaped endpoints there.
Pass --port to override if you've already got Ollama running.
docker run -d \
--name open-webui \
-p 3010:8080 \
--add-host=host.docker.internal:host-gateway \
-v open-webui:/app/backend/data \
ghcr.io/open-webui/open-webui:main
Open http://localhost:3010 and create the first admin account (Open WebUI requires one user at install time).
The
--add-hostflag makeshost.docker.internalresolve to the host's IP on Linux too; macOS/Windows Docker resolves it automatically.winnow gatewayruns on the host's11434, not inside the container.
In Open WebUI:
http://host.docker.internal:11434/v1winnow setup / the
/settings pageBack on the main Open WebUI screen:
You'll see the response stream in. Reload the wiki at any time; the next chat picks up the new context (foundations + recent sample are loaded fresh per request).
Once Winnow ships its own container (Inc 24+), this is the canonical setup. Today, Winnow runs on the host:
# docker-compose.yml
services:
open-webui:
image: ghcr.io/open-webui/open-webui:main
ports:
- "3010:8080"
environment:
# Pre-fill the OpenAI connection so users skip the
# admin-settings click-through. WEBUI_AUTH=False also
# disables Open WebUI's own login (you've already
# got bearer auth on the gateway).
- OPENAI_API_BASE_URL=http://host.docker.internal:11434/v1
- OPENAI_API_KEY=${WINNOW_API_TOKEN}
- WEBUI_AUTH=False
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- open-webui:/app/backend/data
restart: unless-stopped
volumes:
open-webui:
Run alongside winnow gateway:
# `your-token` = a bearer pulled from `winnow setup` / `/settings`.
WINNOW_API_TOKEN=your-token docker compose up -d
winnow gateway
Open http://localhost:3010. Open WebUI starts with the gateway already wired; pick winnow from the model picker.
The same pattern works for any client that takes an OpenAI base URL + API key:
.continue/config.yaml
add models: [- name: Winnow, provider: openai, model: winnow, apiBase: http://localhost:11434/v1, apiKey: <your-token>].http://localhost:11434/v1 with the
bearer token as API key.winnow shows up wherever the client lists discovered models.
HTTP 503 — "tokens not configured" — no bearer tokens stored
in .winnow/config.db. Run winnow setup (interactive) or hit
the Web UI's /settings page → Bearer tokens → Generate token.
The gateway loads Config once at startup (it runs as a
separate uvicorn process), so restart it after generating a
token: kill the running winnow gateway and start it again.
HTTP 401 — "Invalid bearer token" — the token you pasted
into Open WebUI doesn't match any stored token. Tokens are
exact-match; check for trailing newlines or whitespace, or
rotate via /settings and try the new value.
Open WebUI "Verify" fails with connection refused — the gateway isn't running, or it's bound to a port the container can't reach. From inside the Open WebUI container:
docker exec -it open-webui curl http://host.docker.internal:11434/v1/models
Should return the model list. If it doesn't, check that the
gateway is bound to 0.0.0.0 (or pass --host 0.0.0.0) — by
default it binds 127.0.0.1 which Docker's host-gateway
can't reach on Linux.
"winnow" doesn't appear in the model picker — Open WebUI caches discovered models. Settings → Refresh models under Admin Settings → Connections, or restart the Open WebUI container.
The reply takes a while before anything appears — v1 of the
gateway fakes streaming: it gets the full LLM response from
Winnow's existing one-shot LLMClient, then chunks the result
into SSE events. So the first chunk only arrives after the
LLM has finished generating. Native streaming (where the user
sees each token as the LLM produces it) is the top Inc 25
follow-on.
Chat seems to ignore prior turns — v1 only uses the latest user message. Conversation continuity (folding prior turns into the assembled context) is a follow-on. For multi-turn flows, keep questions self-contained.
winnow) — mode-routing via suffixes
(winnow/wiki, winnow/discover) is a follow-on./v1/embeddings — opt-in if Open WebUI's RAG features
want it. Not needed for the chat-only flow.README.md — winnow gateway command
reference, exposed routes, configuration flags.docs/cli-user-guide.md — the gateway
walkthrough in user-guide voice.docs/implementation/to-do.md —
Inc 25 sub-step ladder + locked design decisions + follow-ons.docs/implementation/completed.md
— Inc 25 ledger as sub-steps ship.The fastest way to run Winnow without installing Python locally.
The Dockerfile at the repo root produces a single image
(~520 MB) that runs both the FastAPI backend (:8000) and the
Next.js Web UI (:3000) inside one container, supervised by
tini + a small bash entry-point.
The same image powers self-hosted use today and the hosted SaaS when Phase B of the Hosted-SaaS roll-out ships — running the same product code in both contexts is a deliberate locked decision (Inc 24a).
ANTHROPIC_API_KEY or
OPENROUTER_API_KEY. The container boots without one
(/api/status works), but winnow ingest will fail at the
first LLM call. You can supply this on first run or later via
the Web UI's /settings page once you have the bootstrap
bearer token (see below).The image isn't published to a registry yet. Build it locally from the repo root:
git clone <this-repo> winnow
cd winnow
docker build -t winnow:latest .
That takes 2–5 minutes on a first build (npm install + pip install + frontend compile dominate); subsequent builds reuse the cached layers.
Run it (first-boot — minimal flags, no secrets baked in):
mkdir -p winnow-data
docker run -d \
--name winnow \
--restart unless-stopped \
-p 3000:3000 -p 8000:8000 \
-v "$(pwd)/winnow-data:/data" \
winnow:latest
Now grab the bootstrap bearer token from the container's first-boot logs:
docker logs winnow | grep -A 1 WINNOW_BOOTSTRAP_TOKEN
You'll see a block like:
============================================================
WINNOW_BOOTSTRAP_TOKEN
------------------------------------------------------------
3KsVUz1dVgMgL1aFzQUtSf2pj3hFtQvzGl7iMHNxd7Q
------------------------------------------------------------
Copy that value. It's your first-and-only bearer credential — the entry-point printed it once on first boot and the backend never returns it from any GET endpoint thereafter. Save it somewhere durable (password manager, secret store) before the log rotates.
Visit:
anthropic_api_key /
openrouter_api_key) and any operational tweaks. Use the
bootstrap token in Authorization: Bearer <token> for
webhook ingestion, the MCP HTTP transport, and winnow gateway.Behind the scenes, first boot:
winnow-data/ (wiki / raw / foundations / etc.)
via winnow init.winnow-data/.winnow/config.db (SQLite — Inc 27)
from any env vars present.Subsequent boots: the entry-point is a no-op for both — the DB stays authoritative across restarts.
For automation that needs a known credential at boot, set
WINNOW_BOOTSTRAP_TOKEN on the container — the entry-point
uses that exact value instead of generating one. No-op on
restarts (the DB already carries a token).
docker run -d \
--name winnow \
-v "$(pwd)/winnow-data:/data" \
-e WINNOW_BOOTSTRAP_TOKEN="$(openssl rand -hex 32)" \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
-p 3000:3000 -p 8000:8000 \
winnow:latest
The single mount point is /data inside the container. It maps
to the Winnow project root: raw/, wiki/, .winnow/ (which
holds config.db, state.json, and the inbox + log JSONLs),
and the on-disk winnow.toml if you ever had one (frozen after
27b's migration). The container has no other persistent state
— losing the volume loses your wiki AND your config (LLM keys,
bearer tokens, model picks); keeping it across docker run
invocations means a clean restart picks up where you left off.
To back up: snapshot the host directory you mounted (zip it,
rsync it, commit it to git). Toggling auto_commit = true (via
docker exec winnow winnow config set auto_commit true or the
/settings page) plus a configured git remote means the wiki
auto-versions each pipeline run — for many setups this is the
backup, no other tooling needed.
After Inc 27, the SQLite config store is the source of truth at
runtime — env vars are read on first boot only, then the DB
takes over. Mutate via the Web UI's /settings page or
winnow config set from inside the container.
| Var | Purpose | Required | Read after first boot? |
|---|---|---|---|
ANTHROPIC_API_KEY or OPENROUTER_API_KEY | LLM provider key. Migrated into the DB on first boot. Required for any pipeline run; pipelines that don't fire LLMs (e.g. winnow lint static-only) work without it. | One, eventually | No — DB wins |
WINNOW_BOOTSTRAP_TOKEN | Deterministic first-boot bearer token (Inc 27e). When set, the entry-point uses this exact value; otherwise generates a fresh one and prints to stdout. No-op when the DB already has tokens. | Optional | No |
WINNOW_API_TOKENS | Comma-separated bearer tokens. Migrated into the DB on first boot; ignored thereafter. New self-hosted installs should use the bootstrap token + Web UI for rotation instead. | Optional | No — DB wins |
WINNOW_TRUSTED_PROXY | true to enable trusted-proxy auth mode (Inc 24d) — the backend reads X-Authenticated-User from the request instead of (or alongside) the bearer header. Set when running behind Caddy / Nginx / Tailscale Funnel / Cloudflare Access. | Optional, default off | No — DB wins |
WINNOW_PROJECT_ROOT | Project-root path inside the container. Read at every boot (the entry-point needs to know where /data is mounted). | Optional, default /data | Yes |
Pass them via -e VAR=value on docker run, an env file
(--env-file .env), or a docker-compose.yml environment:
block.
The entry-point dispatches: if you pass arguments after the image name, they're exec'd directly inside the running CLI context instead of booting the services. This means the container also doubles as a Winnow CLI runner without installing Python on the host:
# Show project status (auto-uses the mounted volume's wiki)
docker run --rm \
-v "$(pwd)/winnow-data:/data" \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
winnow:latest \
winnow status
# Run an ingest cycle
docker run --rm \
-v "$(pwd)/winnow-data:/data" \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
winnow:latest \
winnow ingest
# Open a shell inside the running container
docker exec -it winnow bash
These short-lived runs share the volume with the long-running
service container — drop a new file into
winnow-data/raw/sales/ on the host, then either run
winnow ingest from the running container's terminal, or wait
for auto-ingest if you have it configured.
For declarative + repeatable setup:
# docker-compose.yml
services:
winnow:
build: . # or `image: winnow:latest` once built
container_name: winnow
restart: unless-stopped
ports:
- "3000:3000" # Web UI
- "8000:8000" # Backend API
volumes:
- ./winnow-data:/data
# All env vars below are first-boot-only after Inc 27 — the
# DB at .winnow/config.db is authoritative once seeded.
environment:
# Optional: deterministic bootstrap token. Omit to let the
# entry-point auto-generate + log one.
WINNOW_BOOTSTRAP_TOKEN: ${WINNOW_BOOTSTRAP_TOKEN:-}
# Optional: pre-seed the LLM key. You can also set this via
# `/settings` after first boot using the bootstrap token.
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
# Uncomment when running behind a reverse proxy:
# WINNOW_TRUSTED_PROXY: "true"
# Minimum-friction bootstrap — no env file needed.
docker compose up -d
docker compose logs winnow | grep -A 1 WINNOW_BOOTSTRAP_TOKEN
# Or, with deterministic credentials baked in:
echo "WINNOW_BOOTSTRAP_TOKEN=$(openssl rand -hex 32)" > .env
echo "ANTHROPIC_API_KEY=sk-ant-..." >> .env
docker compose up -d
For anything beyond local-dev — TLS, custom domain, a
single port for the public to hit — put a reverse proxy in
front. Caddy, Nginx, Traefik, or any ingress controller
works. Exposed-port 3000 (frontend) and 8000 (backend) need
to be routed separately because the backend's API lives at
/api/* while everything else is the Next.js app.
winnow.example.com {
handle /api/* {
reverse_proxy localhost:8000
}
handle {
reverse_proxy localhost:3000
}
}
If Caddy authenticates users itself (e.g. via
forward_auth against an SSO provider), set
WINNOW_TRUSTED_PROXY=true on the container and have Caddy
inject X-Authenticated-User: <username> on the upstream
request — the backend will treat that as the authenticated
identity (Inc 24d).
server {
server_name winnow.example.com;
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# When auth is upstream + WINNOW_TRUSTED_PROXY=true:
# proxy_set_header X-Authenticated-User $remote_user;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
}
The image ships a Docker HEALTHCHECK that polls
GET /api/status every 30s with a 15s start grace. Status
shows up in docker ps:
STATUS
Up 2 minutes (healthy)
If the backend can't load Config (bad volume mount, missing
permissions, corrupted .winnow/config.db, or — on a fresh
project — a malformed winnow.toml getting migrated) the
healthcheck fails and the container is marked unhealthy.
docker logs winnow shows the exception.
git pull # pull new commits
docker build -t winnow:latest . # rebuild
docker compose up -d # recreate the container
# or:
docker stop winnow && docker rm winnow
docker run -d ... winnow:latest # same flags as before
The volume survives docker rm. Schema changes between
releases are documented in docs/implementation/completed.md's
ledger.
| Symptom | Likely cause |
|---|---|
| Where's my bootstrap token? | First-boot logs only — docker logs winnow | grep -A 1 WINNOW_BOOTSTRAP_TOKEN. If you missed it: docker exec -it winnow winnow setup will let you generate a new bearer interactively, or docker exec -it winnow winnow config set api_tokens '["new-token"]' rotates the list. |
HTTP 503 from /api/raw/{subdir}, /api/mcp, or /v1/chat/completions | No bearer tokens configured in the DB. Run docker exec winnow winnow setup and pick the regenerate option, or hit the /settings page's Bearer tokens card. |
HTTP 401 on the same routes | Token mismatch. The token in Authorization: Bearer <…> doesn't match any stored token. Tokens are exact-match — check for trailing whitespace. Use /settings to rotate or docker exec winnow winnow config get api_tokens to inspect (treat the output as secret). |
| Container starts then immediately exits | docker logs winnow will show the cause. Common: missing /data write permissions (the entry-point can't run winnow init / winnow setup), or — on first boot — a malformed winnow.toml being migrated into the config DB. |
/api/status healthcheck failing | Backend can't load Config. Check docker logs — usually a missing project structure under /data, a corrupted .winnow/config.db, or (first boot only) a malformed winnow.toml. |
| Web UI loads but every action fails | Frontend talks to /api/* on the same origin. If your reverse proxy isn't forwarding /api/* to port 8000, requests fall back to the Next.js dev server and 404. Re-check the proxy config. |
winnow ingest errors with LLMError | LLM key missing or wrong. Both the boot and HTTP serving don't call the LLM, so they succeed without a key — winnow ingest is the first thing that does. |
README.md — quick-start + concepts +
reference for every command and route.docs/cli-user-guide.md — the
CLI walkthrough (works inside the container too).docs/web-user-guide.md — the Web UI
walkthrough.docs/open-webui-integration.md
— pointing Open WebUI at winnow gateway from a sibling
container.Winnow is open-source and free to run on your own infrastructure. The hosted version at usewin.now is opening in waves — join the waitlist to get early access.