Skip to content

Config reference

Every key of profiles.yaml, grouped by block. Validation is strict: unknown keys are rejected (extra="forbid"), so typos fail loudly at startup.

Env-var settings (DISCOVER_*) live in Environment variables. ${VAR} / ${VAR:-default} expansion applies to every value.

Profile

Key Default What it does
id required Slug (a-z0-9_-); feed at /p/{id}; unique across profiles
name required Display name
description "" Shown on the profile page
topics [] Feed tabs — see topics
keywords [] Profile-level keyword list
sources [] Feeds and API adapters — see sources
alert_rules [] Keyword alert rules, seeded once — see alert rules
alerts defaults Notification channels + settings — see alerts
llm required summarizer + chat roles — see LLM endpoints
summaries defaults Brief/report behaviour — see summaries
enrichment disabled Web-search enrichment — see enrichment
context disabled Background/context links — see context

Topics

Key Default What it does
name required Tab label; unique per profile (case-insensitive)
include [] Whole-word, case-insensitive keywords; empty = every article
exclude [] Any match excludes the article (wins over include)
sources [] Restrict to these source names; empty = all of the profile's
enabled true Disable without deleting

Sources

Key Default What it does
id slug of name Stable id shared across profiles; set it explicitly to survive renames
name required Display name
type rss rss, atom, web, api, guardian, newsdata, x
url API home for API types Feed URL; required for rss/atom
enabled true Disabled sources are not fetched
refresh_minutes 30 Fetch interval (plus jitter)
tags [] Labels (exported as OPML category)
aggregator false Feed links to other publishers (HN): citations show domain "via "
first_party false Publisher is the subject (SEC/Fed, blogs): claims attributed to it
params {} API query — guardian q/section/from_date/page_size/max_pages, newsdata q/country/language/category/max_pages, x query/users (all allow base_url)
api_key unset Env var name holding the key (api_key: ${GUARDIAN_API_KEY})
daily_request_budget unset Stop calling after this many requests per UTC day
max_posts_per_run unset Cap items kept per fetch (1-100)
monthly_budget_usd unset X: hard spend cap per UTC month — required to enable an X source
cost_per_read_usd 0.005 X: price per read for budget accounting
social false Short social posts: no extraction, no reports (set automatically for x)

Alert rules

Key Default What it does
name required Rule name; unique per profile
enabled true
include required Keywords; $AAPL = case-sensitive ticker also matching "AAPL"
case_sensitive false Whole-rule override: match include/exclude verbatim
exclude [] Wins over include
match any any include matches, or all must
match_in [title, summary] Add text to re-check after extraction
sources [] Source names to restrict to; empty = all the profile's
priority default low / default / high (maps to ntfy priority 2/3/5)
channels [] Channel ids from alerts.channels
quiet_hours unset {start: "22:00", end: "07:00"} local alerts.timezone; held → digest
max_per_hour 10 Overflow is held and sent as one digest
notify_on_update false One update (max hourly) when a new source joins an alerted story

Alert channels

Key Default What it does
id required Slug; referenced by rules' channels
type required webhook, ntfy, email
name id Display name
enabled true
timeout_seconds 10 Send timeout (max 120)
url required for webhook JSON POST target
secret unset ${ENV} name — HMAC key for X-Discover-Signature
server https://ntfy.sh ntfy server root
topic required for ntfy ntfy topic
token unset ${ENV} name — bearer token for protected topics
host required for email SMTP host
port 587 SMTP port
tls starttls starttls / ssl (port 465) / none
username, password unset ${ENV} names — SMTP login
sender required for email From address
to required for email Recipient list

Alerts block

Key Default What it does
channels [] Channel list (above); seeded once, then managed in the UI
alert_max_age_hours 6 Older articles never alert (no backfill floods)
notification_retention_days 30 Notification log retention
max_attempts 5 Send attempts with exponential backoff (max 20)
timezone UTC IANA zone for quiet hours and notification times
public_url http://localhost:8000 Base URL for story links in notifications

Summaries

Read from the YAML on every start (not stored in the DB).

Key Default What it does
card_style lead_bullets lead_only shows cards without bullets
report_mode auto off publishes with skipped(reports_off); on_demand reads as auto
publish_requires_report true Publish only once the report is final (ready/skipped/failed)
publish_max_wait_minutes 30 Then publish with "Detailed report still generating"
brief_fallback_hours 6 A failed brief publishes with the feed snippet after this
extraction_wait_seconds 120 Cluster a story after this long even if text is pending
max_inflight 1 Concurrent LLM requests, all kinds, process-wide
summarize_backfill_hours 24 Unpublished stories older than this are not generated
summarize_backfill_max 40 At most this many unpublished stories are worked on
report_max_articles 4 Members sent to a report prompt (one per source first)
summarize_max_articles_per_story 5 Full-text members picked for a brief (the rest are listed, not dropped)
summarize_max_articles_per_story_deep 8 Same cap for deep-tier stories; also their report article cap
report_total_words 2400 Source words per report, shared by its articles
report_max_tokens 1400 max_tokens for report requests (briefs use the role's)
report_timeout_seconds 300 Timeout for report requests
duplicate_threshold 0.6 Bullet/paragraph token overlap that counts as duplicate
verbatim_max_run 12 Reject a report copying this many consecutive source words
verbatim_max_ratio 0.20 Or with more than this share of copied 8-word phrases
verbatim_allow [] Terms copied runs may consist of (title words always allowed)
retry_temperature_bump 0.2 Added to temperature on validation retries
event_retention_days 7 story_events kept for stream resume
chat_context_words 3000 Source words sent with a Q&A question
chat_history_turns 6 Earlier Q&A pairs sent with a question
chat_retention_days 30 Idle conversations deleted after this
chat_priority true Chats block new generation requests on the shared endpoint
chat_max_concurrent 1 Chats streaming at once per profile
followups true 3 suggested questions per story (needs llm.chat)
report_concurrency, max_reports_per_run, report_auto_max_age_hours, report_max_words_per_article — Deprecated, accepted but ignored

Enrichment

Per profile (enrichment:). The UI toggle in Settings > Pipeline overrides enabled for an existing database. Thresholds and providers come from the DISCOVER_SUMMARIZE_PROFILE profile (else the first).

Key Default What it does
enabled false
providers [tinyfish] Ordered search providers — see providers
target_sources 3 Stories with fewer distinct publishers are searched
max_age_hours 48 Only stories updated this recently
max_added_sources 3 Max independent publishers added per story
wait_seconds 180 How long a new story's brief waits for enrichment
concurrency 2 Stories searched at once
min_snippet_similarity 0.25 Hit title+snippet vs headlines (TF-IDF cosine)
min_text_similarity 0.35 Extracted page text vs the story
min_shared_entities 2 Shared names/numbers (rule B)
min_snippet_strong 0.45 Rule B: snippet this strong can carry weaker text
min_text_floor 0.25 Rule B: text must still reach this
link_only false Keep strong-snippet unfetchable hits as link-only members
headline_only_title_similarity 0.50 Unfetchable hits with a title this similar (plus entities, below) become headline-only members
headline_only_min_entities 2 Shared names/numbers a headline-only member needs
depth on Deep coverage tier — see depth
max_time_gap_hours 72 Publication-time gap vs the story
duplicate_overlap 0.6 5-word shingle overlap that counts as a copy
deny_domains [] Blocked candidate domains
social_domains [] Extra hosts treated as social
aggregator_domains [] Extra hosts treated as aggregators
publisher_aliases bbc.co.uk→bbc.com, guardian.co.uk→theguardian.com Domains that are the same publisher
extra_rules on Rules C/D below
quality on Source-quality gate — see quality
reenrich_hours 24 Admin "enrich now" ignores stories enriched more recently (unless story_id)
query_mode heuristic llm asks the summarizer for queries
resummarize_on_enrichment true Late sources regenerate a published story once
cache_hours 24 Search-result cache
language en Hits in another language are rejected
max_candidates 10 Candidates fetched per story
max_queries_per_story 2 Requests per provider per story
min_title_similarity 0.30 Snippet-less hits (GDELT): title similarity + shared entity
empty_cache_minutes 10 Empty results cached this long at most (0 = never)
second_pass_hours 3 Under-target stories searched again after this (0 = off)
late_regen_max_per_hour 6 Cap on enrichment-triggered regenerations
skip_title_patterns ^Show HN, ^Ask HN, ^Tell HN, ^Opinion, ^Letters Headline regexes: never searched
strip_title_prefixes Show HN, Analysis, Exclusive, Live, … Boilerplate removed before building queries
min_signal_tokens 1 Headlines with fewer distinct names/numbers are skipped (0 = off)
skip_url_patterns /commentisfree/, /opinion/, /opinions/, /comment/, /columnists/, /blogs/, /live/ URL-path substrings: searched and never match

extra_rules

Key Default What it does
enabled true Rules C and D accept real coverage scoring under min_text_similarity
rule_c_entities 5 Rule C: shared names/numbers …
rule_c_text 0.25 … plus text similarity at least this → entities+c
rule_d_title 0.55 Rule D: hit title this similar to the story headlines …
rule_d_entities 3 … plus this many shared entities …
rule_d_text 0.22 … and text at least this similar → title+d

Enrichment quality

The source-quality gate: accepted candidates are flagged, never rejected. Confirmed farms in deny_domains are hard-denied.

Key Default What it does
suspicious_tlds edu.pl, club, shop, online, site, xyz, top, icu, info, biz, click, link, work, buzz, vip, website, space, today, live Domain endings that flag a hit (tld:<suffix>)
suspicious_patterns (news\|wire\|daily\|hub\|insider\|feed) token on the registrable domain Keyword-stuffed farm names flag (name:<token>); fused names like wired.com do not
deny_domains farms confirmed in live evals Hard-denied like deny_domains; a YAML list replaces the defaults entirely

Enrichment depth

The deep coverage tier (enrichment.depth): important stories get more queries, pages and per-country repeats so they end up with many distinct outlets; niche stories keep the cheap standard behavior. A story is deep when its feed members span deep_min_publishers distinct publishers, its priority exceeds deep_min_priority, or an alert already fired on it — and enabled/daily_request_budget still admit it.

Key Default What it does
enabled true Off: every story is enriched on the standard tier
deep_min_publishers 2 Deep tier at this many distinct feed publishers
deep_min_priority unset Deep tier above this story priority (unset: priority never admits)
deep_on_alert true Deep tier for stories an alert rule already fired on
target_sources 12 Distinct publishers a deep story is searched for up to
max_added_sources 12 Web members added per pass on the deep tier
max_queries_per_story 4 Query variants per provider (entities, paraphrase, key numbers, action phrase)
pages 2 Result pages per query (TinyFish page 0..n-1); an empty page stops early
locations [US, GB, AU, CA, IN] Country codes the strongest query is repeated for (TinyFish location); [] = off
daily_request_budget 1500 Deep-tier requests/day across providers (enrich:deep usage row); unset = unlimited
ensure_groups [] Outlet groups (outlets.yaml) a deep story should cover; missing groups get one include_domains query each
max_group_queries 3 At most this many ensure-groups queries per story

Deep-tier candidates follow the same acceptance rules; headline-only members (headline_only_title_similarity/headline_only_min_entities above) count separately and are never summarized or cited. The Pipeline panel shows the sources-per-story distribution; python -m app.enrich.evaluate --deep forces every story onto the deep tier for evaluation.

Search providers

Each entry under enrichment.providers / context.providers (SearchProviderConfig):

Key Default What it does
name required tinyfish, gdelt, searxng, guardian, newsdata, brave, exa
base_url provider default API root; required for searxng (with json in search.formats)
api_key unset Env var name holding the key
budget_source unset Share another source's daily request budget
daily_request_budget unset Requests/day cap, counted in source_usage as search:<name>
monthly_budget_usd unset Monthly spend cap
cost_per_request_usd 0.0 Price per request for the spend cap
min_interval_seconds tinyfish 2, gdelt 10, else 1 Seconds between requests (gdelt floored at 5; tinyfish at 2 unless override_min_interval)
override_min_interval false Allow tinyfish min_interval_seconds below 2
search_type news tinyfish domain_type: news or web
max_results 10 Results requested per query (max 50)
timeout_seconds 15 Per-request timeout
fetch_url unset TinyFish Fetch endpoint used by context.fetch_via (planner mode)

Context

Per profile (context:). Domain rules (deny_domains, social_domains, aggregator_domains, publisher_aliases) come from the profile's enrichment block.

Key Default What it does
enabled false
providers [tinyfish] Same provider registry; exa also does find_similar
max_angles 3 Search angles generated per story
max_items 3 Context links kept per story
max_per_domain 1 Accepted links per story from one domain (domain_cap)
min_words 300 Pages extracting less are rejected
similarity_floor 0.10 Below: off_topic
similarity_ceiling 0.45 At/above: same_event (must be > floor)
min_shared_entities 1 Shared names/numbers required
min_shared_entities_heuristic 2 Same, for heuristic (non-LLM) angles
min_priority 0 Eligible when story.priority is above this …
min_publishers 2 … or it has this many distinct publishers
daily_request_budget 30 Context's own cap (context:<name> usage rows)
llm_judge true LLM scores pages 0/1/2; keeps only adds_context: 2
reject_flagged true Drop candidates the quality gate flags
max_opinion 1 Opinion/column links kept per story (0 = none)
same_event on Deterministic same-event signals — below
language en Hit language must match when reported
cache_hours 24 Search-result cache
empty_cache_minutes 10 Empty results cached this long at most
max_age_hours 48 Stories eligible while published within this
concurrency 2 Stories processed at once

Planner mode

query_mode picks the pipeline: planner (default; LLM-picked tasks from a fixed catalog -> verified 'Additional detail' items) or angles (the similarity/judge pipeline above — its rows apply only there; max_tasks, max_extracted and the actions map below apply only to planner).

Key Default What it does
query_mode planner planner or angles (rollback path)
max_tasks 3 Retrieval tasks the planner may pick per story
max_queries_per_task 2 Queries each task's recipe may run
max_judged 8 Candidates sent to the extractor per story
max_extracted 8 Pages fetched + LLM-extracted per story
fetch_via unset tinyfish: Fetch-API fallback for 403/empty/timeout pages
archive true Allow older-window queries (items get origin: archive)
actions {} Per-task-type overrides — below

actions (per task type: history/quotes/financials/related_developments/background)

Key Default What it does
enabled true Turn one task type off
max_queries unset Override max_queries_per_task
pages_per_query 2 Hits kept per query
window_days per type Recent window override (quotes 90, financials 180, related 30)
min_age_days 3 A dated page newer than this is rejected for history
research false background: use domain_type: research_paper + pub_year bounds

same_event rules

Key Default What it does
numbers_min_shared 2 Shared numeric tokens (no single digits or 4-digit years) → same_event
numbers_text_chars 600 Page prefix scanned for numbers (title + first N chars)
window_hours 72 Published within ± this of first_at = contemporaneous
undated_min_entities 3 Undated pages sharing this many entities count as contemporaneous
max_title_similarity 0.30 Contemporaneous page must stay under this title similarity …
max_text_similarity 0.30 … and this text similarity (both)
entities_min 3 Title shares this many story entities AND an event word → same_event
event_terms verbs/nouns list Event words (announce, launch, acquire, breach, verdict, …; full list DEFAULT_EVENT_TERMS in app/context/rules.py)

Perspectives

Per profile (perspectives:) — the "how outlets differ" digests and synthesis (feature page). A story can also opt out per profile from the pipeline settings page.

Key Default What it does
enabled false
min_sources 4 Synthesize only with at least this many member digests
max_per_hour 6 Perspectives syntheses per hour (rate limit like late regenerations)
max_articles 12 At most this many members are digested per story
digest_max_words 1400 Article words sent to the digest prompt
max_age_hours 168 Stories eligible while published within this

Images

Top-level images: block:

Key Default What it does
cache true Download validated images once, serve resized WebP from /img/{key}
max_mb 500 LRU cap of the image cache
no_image_style logo logo / text_only / gradient fallback (DISCOVER_NO_IMAGE_STYLE overrides)
backfill_days 7 Backfill images of stories published this recently
backfill_per_run 20 Articles per backfill run
backfill_interval_minutes 10 images-backfill job interval
per_run 50 New articles checked per pipeline run
timeout_seconds 10 Image fetch timeout
max_bytes 3145728 Image size cap (3 MB, header and streamed)
min_width, min_height 320, 180 Smaller images rejected
tiny_feed_width 640 Page image wins over a feed image narrower than this
cache_width 640 Cached WebP width
generic_threshold 3 Same URL on this many articles of one domain = generic
max_pixels 40000000 Decompression-bomb guard (Pillow)

Telemetry

Top-level telemetry: block — the Workflow diagnostics data collection. Off by default? No: enabled defaults to true; the Settings > Workflow toggle overrides it at runtime and persists.

Key Default What it does
enabled true Collect llm_calls/stage_events rows
retention_days 14 Both tables pruned daily to this age
store_payloads failures none / failures / all — prompt+response previews kept for the inspector
payload_max_chars 4000 Each payload preview clipped to this
redact true Strip API keys and common secret patterns from stored payloads