You are running an automated rolling 7-day GA4 + Search Console + SERPRobot report for luottoriskit.fi. Use only these data sources: MCP servers: * Google analytics MCP * Google search console MCP * Google Sheets MCP * Email report MCP REST API (HTTP GET only): * SERPRobot rank tracking API — only the exact `project_report` endpoint defined in the "SERPRobot Rank Tracking" section below. Do not search the web, do not browse arbitrary pages, and do not use any other MCP tools or connectors. The SERPRobot `project_report` endpoint defined below is the ONLY allowed HTTP request outside the MCP servers (called once per comparison window). Assume correct credentials and permissions are already configured. The SERPRobot API key is available as the environment variable SERPROBOT_API_KEY. Do not ask for the Google Cloud project ID or for the SERPROBOT_API_KEY during the report run. ## GA4 Property * Site: luottoriskit.fi * Property ID: 536131777 ## Search Console Property Primary Search Console property: * sc-domain:luottoriskit.fi If this property fails, list available Search Console properties through Google search console MCP and use the closest verified luottoriskit.fi property. Do not invent a property URL. ## SERPRobot Rank Tracking Target keyword rank tracking is provided by the SERPRobot API. * Project: SuomiFinder (project_id 5117674) * Tracked domain: luottoriskit.fi * Search region: www.google.fi * Endpoint (HTTP GET), action `project_report`: https://api.serprobot.com/v1/api.php?api_key={{SERPROBOT_API_KEY}}&action=project_report&project_id=5117674&start={START}&end={END} Substitute {{SERPROBOT_API_KEY}} with the SERPROBOT_API_KEY environment variable. Never print the API key in the report, the email, or the chat. Parameters: * `start` and `end` accept YYYY-MM-DD, or the strings "today" / "yesterday", or "XdaysAgo" (e.g. "7daysAgo"). Default if omitted: start = yesterday, end = today. * Call the endpoint TWICE per run, using the same ISO date windows as GA4 and Search Console: * Viikko A call: start = Viikko A start, end = Viikko A end * Viikko B call: start = Viikko B start, end = Viikko B end * Do not call any other SERPRobot action or project, and do not invent additional parameters. Note: SERPRobot tracks rankings on its own schedule and is not subject to the GA4 / Search Console 2-day processing lag. The windows are aligned to Viikko A and Viikko B only for a consistent comparison; per-keyword freshness is given by the `updated` field. The response is JSON with `id`, `start`, `end`, and a `report_data` array. Relevant per-keyword fields: * `keyword` — the tracked search term * `keyword_id` — stable id; use it to match the same keyword across the two windows * `earliest_position` — rank at the start of the window (null = not ranking in the tracked range during the window) * `latest_position` — rank at the end of the window; this is the keyword's standing for that window (null = not ranking) * `change` — latest_position minus earliest_position within the window * `best_ever_position` / `first_ever_position` — historical context (may be null) * `found_serp` — the ranking URL on luottoriskit.fi for that keyword ("" when not ranking); use it to tie keywords to specific pages * `volume_local` / `volume_global` — monthly search volume (string; "-" means no volume data) * `cpc_local` / `cpc_global` — cost-per-click estimate (commercial value signal) * `cmp_local` / `cmp_global` — competition index 0–100 * `updated` — timestamp of the most recent check for that keyword Sign convention (lower position number is better): * `change` = latest_position − earliest_position. A NEGATIVE change means the rank number went down, i.e. the keyword IMPROVED → "sijoitus parani". A POSITIVE change means it WORSENED → "sijoitus heikkeni". * Use `change` only when both earliest_position and latest_position are non-null; otherwise treat within-window movement as unavailable. Important characteristics and handling: * The number of tracked keywords = the number of rows in `report_data` (state it; expected around 82). * null position = "ei sijoitusta seuratulla alueella", not zero. Treat high-volume keywords with null or weak positions as priority opportunity gaps. * `volume_local` / `volume_global` = "-" means missing; do not coerce to 0 and do not fabricate. * Use only real values returned by the API. Never invent, estimate, or interpolate positions, changes, or volumes. * If either SERPRobot call fails, times out, or returns invalid/empty JSON, include the exact error or HTTP status in the report and continue with GA4 + Search Console reporting (and with the other window if only one of the two failed). The report must still be sent. ## Yrityspaneeli (Google Sheets) A fixed panel of company pages is tracked over time for per-page Google ranking (section 15). The panel list lives in a Google Sheet, read via Google Sheets MCP: * Spreadsheet ID: 1HKywNF7FiTEvl_J31ufol382KGSDy2K2w02uA8XQ9t8 (the whole document ID from the URL /d//edit, not a single tab) * Tab (worksheet) name: paneeli (referenced by name, e.g. range paneeli!A:A) * Column A holds one company-page URL per row in canonical form (e.g. https://luottoriskit.fi/fi/yritykset/3269212-8/oy-bws-group-ab/). Read column A; if the first cell is a header rather than a URL, ignore it. Read this list ONCE at the start of the run and reuse it for section 15. The panel is fixed: do not add, drop, reorder, or substitute URLs. If the Sheet read fails, skip section 15, state the exact error in that section, and continue with the rest of the report. ## Muisti (Google Sheets) Two additional tabs in the SAME spreadsheet (Spreadsheet ID above) give the routine persistent memory, so discoveries and recommendations carry across runs instead of repeating forever. Read and write them via Google Sheets MCP. Row 1 of each tab is a header; data starts on row 2. ### Tab: modifierit Persistent modifier vocabulary plus discovery staging. Columns: * A modifier — short name (e.g. "tase") * B tokens — the contains token(s) for this modifier, comma-separated (e.g. "tase, taseen loppusumma"); a query matches the modifier if it contains ANY token * C exclude_tokens — optional generic phrasings to subtract (via equals/notContains), comma-separated * D status — active | candidate | rejected * E first_seen — date the modifier was first proposed (YYYY-MM-DD) * F volyymi — rough volume / source (e.g. "3600/kk") * G esimerkkihaut — 1–2 example queries * H huomiot — free notes How the routine uses it: * At run start, read all rows. Treat status=active rows as ADDITIONAL single-modifier filters, merged with the built-in list in "Data to retrieve from Search Console → 3", and add their tokens to the "pelkät yritysnimihaut" notContains exclusion set. The built-in list is the always-available baseline; the sheet only adds to it. * The routine may ONLY append rows with status=candidate. It must NEVER write status=active and never overwrite or edit existing rows. Promoting candidate → active is a human step (after checking the tokens), so a bad entry can never break the next run's queries. * Deduplicate: before proposing a modifier in section 7.1, check it is not already present (active OR candidate). If it is, do not re-add it; note "jo ehdotettu (first_seen X), odottaa hyväksyntää" instead. ### Tab: viikkoloki Append-only time series, one row per run. Columns: * A ajopvm — run date (YYYY-MM-DD) * B jakso — Viikko A date range * C avainluvut — compact key metrics (e.g. sessions, organic clicks, average position, key events) * D havainnot_ja_suositukset — the run's notable findings and the 3 recommendations (section 14), condensed * E seuranta_edellisiin — follow-up on items flagged in earlier rows (e.g. "AI-checkout romahti 15.7 → nyt palautunut") * F avoimet_seurattavat — items still open to watch next run How the routine uses it: * At run start, read the last 2–3 rows to write this run's follow-up (section 16) and to fill column E. * After the report is composed, append exactly ONE new row for this run. Never edit previous rows. ## Business context Use this context only when interpreting the GA4, Search Console, and SERPRobot data and writing recommendations: * Site: luottoriskit.fi * Main business goal: increase qualified traffic and leads for credit risk, company credit reports, risk assessment, and company valuation tools * Important page types: credit risk pages, credit report pages, company pages, company valuation pages, pricing pages, support pages, FAQ pages, guide pages, model explanation pages, onboarding pages * Desired user actions: view product/tool pages, read support or FAQ content, compare pricing, start onboarding, contact the company, request access, show intent toward company credit reports or valuation tools Important SEO context: Most organic traffic to luottoriskit.fi comes from long-tail company-related searches, not from generic head terms. The company name in these searches is unbounded and unpredictable, but the modifier (the non-name part) is a small, stable vocabulary. Typical patterns: * "[company name]" * "[company name] luottoriski" * "[company name] luottoluokitus" * "[company name] luottotiedot" * "[company name] taloustiedot" * "[company name] liikevaihto" * "[company name] roe" * "[company name] y-tunnus" * "[company name] konkurssi" * "[company name] maksuhäiriö" Treat these long-tail company searches and company+modifier searches as the core traffic model of the site. Generic keywords such as "luottoriski" or "yrityksen luottoluokitus" are useful, but they must not dominate the analysis unless the data clearly shows they dominate traffic or business value. Do not interpret low CTA or purchase event rates as automatically negative for all organic traffic. Many long-tail company searches are informational lookups (revenue, ROE, business ID, basic facts). For these searches, success may mean that the visitor quickly found the answer, not that they bought a report immediately. Segment CTA interpretation by likely intent: * informational lookups: company name, y-tunnus, liikevaihto, liikevoitto, käyttökate, roe, roa, taloustiedot, tilinpäätös * risk-aware research: luottoriski, luottoluokitus, luottotiedot, maksuhäiriö, verovelka, konkurssi, saneeraus * commercial / report intent: luottoriskiraportti, raportti, pricing, onboarding, request access Only recommend stronger report CTAs where the query category or landing page suggests risk-aware or commercial intent. For informational lookups, prefer softer next steps such as related metrics, internal links, explanation snippets, or "tarkista myös luottoriski" modules. ## Date ranges to compare The routine should run every day, but it must compare rolling 7-day periods instead of single calendar days. Because Google Analytics and Search Console data can be delayed, do not use the current date as the end of the latest comparison period. The latest comparison period must end 2 days before the local run date. Calculate: * Viikko A: the most recent complete 7-day period ending 2 days before the local run date * Viikko B: the 7-day period immediately before Viikko A State both date ranges explicitly in the report. To calculate the dates programmatically: * Today = the local run date * Viikko A end = today minus 2 days * Viikko A start = today minus 8 days * Viikko B end = today minus 9 days * Viikko B start = today minus 15 days These ranges are inclusive. Example: if Today is 25.6.2026, then Viikko A is 17.6.2026–23.6.2026 and Viikko B is 10.6.2026–16.6.2026. Use ISO dates for GA4 and Search Console API queries. In the report, show dates as d.m.yyyy. Note: the SERPRobot `project_report` endpoint is queried with these same date windows (one call for Viikko A, one for Viikko B), so SERPRobot rankings are compared week-over-week like the other sources. ## Data to retrieve from GA4 Use Google analytics MCP run_report with property ID 536131777. Retrieve the following for BOTH date ranges separately. ### 1. Overall traffic, no dimension Metrics: * activeUsers * totalUsers * newUsers * sessions * engagedSessions * engagementRate * averageSessionDuration * screenPageViews * eventCount * keyEvents ### 2. New vs. returning users Dimension: * newVsReturning Metrics: * activeUsers * sessions * engagedSessions * engagementRate ### 3. Channel breakdown Dimension: * sessionDefaultChannelGroup Metrics: * activeUsers * sessions * engagedSessions * engagementRate * screenPageViews * eventCount * keyEvents ### 4. Source/medium breakdown Dimension: * sessionSourceMedium Metrics: * sessions * activeUsers * engagedSessions * engagementRate * keyEvents Return top 10 rows by sessions for both periods. Compare the union of rows appearing in either Viikko A or Viikko B. ### 5. Top pages by pageviews Dimension: * pagePathPlusQueryString Metrics: * screenPageViews * activeUsers * engagedSessions * engagementRate * eventCount * keyEvents Return top 15 rows by screenPageViews for both periods. Compare the union of rows appearing in either Viikko A or Viikko B. ### 6. Landing pages Dimension: * landingPagePlusQueryString Metrics: * sessions * activeUsers * engagedSessions * engagementRate * eventCount * keyEvents Return top 20 rows by sessions for both periods. Compare the union of rows appearing in either Viikko A or Viikko B. Use these rows to compare engagement by page type (company pages vs. content pages such as /fi/luottoriski/, /fi/faq/, guide pages, pricing pages, and onboarding pages). ### 7. Events Dimension: * eventName Metrics: * eventCount * keyEvents * activeUsers * sessions Return top 20 rows by eventCount for both periods. Compare the union of rows appearing in either Viikko A or Viikko B. If any GA4 metric or dimension name is rejected by the API, retry with the nearest valid GA4 Data API equivalent. Use only real data returned by the API. Never invent, estimate, interpolate, or assume numbers. ## Data to retrieve from Search Console Use Google search console MCP for Search Console property sc-domain:luottoriskit.fi. If this property fails, list available Search Console properties through Google search console MCP and use the closest verified luottoriskit.fi property. If no verified property is available, continue with GA4-only reporting. Token budget — keep the run cheap: the biggest token cost is large row payloads pulled into context, NOT the number of small aggregate calls. So: (1) keep the clicks>0 main pull at rowLimit 1000 and the impressions pull at ~100; do not paginate. (2) The modifier category filters return only totals (a few numbers each) — they are cheap, so prefer them over pulling more raw rows. (3) Never echo raw row lists into the report; summarise to the notable rows only. (4) Do not re-fetch the same query more than once; reuse results across sections. (5) If you approach a size/limit concern, drop the least important detail (extra example rows, minor categories) rather than the core metrics. Retrieve the following for BOTH date ranges separately. ### 1. Organic search trend Dimension: * date Metrics: * clicks * impressions * ctr * position Retrieve daily rows for both 7-day periods. Use these rows to check data availability by date and to summarize the weekly trend. For the weekly comparison, aggregate clicks and impressions across each 7-day period. Calculate weekly CTR as total clicks divided by total impressions when mathematically valid. Use the Search Console period-level average position if available; otherwise calculate an impressions-weighted average position from the returned date rows when mathematically valid. Also retrieve total clicks and impressions for each period with NO query dimension and NO query filter. This is needed for the invisible-tail estimate described below. ### 2. Organic search queries — exact rows for discovery and SEO targeting Dimension: * query Metrics: * clicks * impressions * ctr * position Main pull — CLICKED queries only (clicks > 0): fetch query rows for both periods filtered to clicks > 0. Filtering to clicks > 0 drops the huge zero-click impression tail and keeps the payload small; it does NOT affect the invisible-tail math, because zero-click rows add 0 to the click sum. Token-efficient target: request `rowLimit = 1000`. For this site that captures the large majority of clicks in a single small request; the clicked tail beyond 1000 rows is mostly 1-click queries, so truncating it changes the click sum only slightly. If you truncate, mark the invisible-tail figure approximate. (The Search Console API ceiling is 25000 rows and you could paginate with `startRow`, but do NOT do that here — the goal is to keep token usage low. Only raise the limit if a run specifically needs a more exact invisible-tail figure.) If the MCP tool caps lower, use its cap. Always state the actual row count returned and whether it was capped. Supplementary pull — high-impression queries (for SEO gaps): the goal is to surface high-impression / low- or zero-click queries. IMPORTANT: the Search Console API has NO sort-by-impressions option — it always returns rows in click order, so a naive "top 100 by impressions" request actually returns the top by CLICKS and misses high-impression zero-click queries. To get these with the current tool: fetch a broader query set WITHOUT the clicks>0 filter at a higher rowLimit (e.g. ~1000), then sort LOCALLY by impressions and keep the top ~20–30. This surfaces high-impression weak-CTR/weak-position SEO targets that the clicks>0 main pull misses. Use it ONLY for that; do not use it for the click sum or invisible-tail. Note that rare per-company queries stay anonymized regardless, so this still cannot recover the full impression tail. Do NOT echo all fetched rows into the report or reproduce the full list. Use the rows only to (a) compute the total click sum for the invisible-tail estimate (from the clicks>0 main pull), and (b) extract the notable rows (top queries, biggest movers, weak-CTR/weak-position SEO targets, new modifiers). Keep only these in the output. Compare the union of queries appearing in either Viikko A or Viikko B. Use exact query rows for SEO targeting and discovery: * surface concrete SEO targets at exact-term granularity: exact company+modifier queries with impressions but weak position or weak CTR * discover modifiers not yet covered by the term list below * label the head/brand/company/modifier mix * identify top queries by clicks, largest click increases/declines, high impressions with weak CTR, and improving/worsening average position Do NOT use this exact-row list as the primary aggregation source for long-tail categories. Use category-filtered Search Console calls for comparable week-over-week trends. If rowLimit is capped by the MCP tool, state the cap and note that the exact-row view is partial. ### 3. Organic search queries — modifier-filtered per-term aggregation Run one aggregated Search Console query per tracked search term (flat list below), using the query dimension filter. The MCP supports contains (for the term), equals (for the exact generic), and notContains (for exclusions) — use these; it does NOT support regex. Retrieve clicks, impressions, ctr, and position for each term for BOTH Viikko A and Viikko B. IMPORTANT — granularity: use NARROW, single-modifier filters, not broad buckets. For SEO it matters whether the search is "[company] liikevaihto" vs. "[company] liikevoitto", so keep those as SEPARATE filters/rows. Broad grouping (e.g. "taloustiedot" or "tunnusluvut") is only an optional roll-up for the summary, never the primary unit. Report the brand FIRST, because "luottoriski" also matches the brand "luottoriskit". Concretely (the current MCP supports notContains even without regex): report the brand as its OWN row using the brand filter, and EXCLUDE the brand from the luottoriski term by combining `contains "luottoriski"` with `notContains "luottoriskit"`. This stops brand searches from inflating the luottoriski figure. Apply the same notContains wherever a modifier string is a substring of the brand. Matching is case-insensitive, so "ROE"/"roe" and "RATING"/"rating" match the same term. In addition to the built-in list below, include any status=active rows from the `modifierit` sheet tab (see "## Muisti (Google Sheets)") as extra single-modifier filters, and add their tokens to the "pelkät yritysnimihaut" notContains exclusion set (see below). The list below is the always-available baseline that works even if the sheet is empty or unreadable. Search terms to track — a FLAT keyword list, NOT categories. Measure each term separately via contains (contains "liikevaihto", contains "quick ratio", …), each on its own row. The brand "luottoriskit" is NOT in this list — nobody searches "[yritys] luottoriskit" — so it is handled on its own row (see brand instruction above) and appears only in the notContains exclusion set for the pure-name estimate. * luottoriski * riskiluokitus * riskiraportti * luottoluokitus * rating * luottotiedot (also match the spelling variant "luottotieto") * luottokelpoisuus * maksuhäiriö (also match "maksuhäiriöt") * verovelka * ulosotto * perintä * konkurssi * saneeraus * taloustiedot * tilinpäätös * liikevaihto * liikevoitto * käyttökate * tulos * roe * roa * omavaraisuusaste * quick ratio * current ratio * maksuvalmius * y-tunnus (also match "ytunnus", "yritystunnus") * tase * tuloslaskelma * ebitda * ebit * roi * roce * yrityshaku (also match "yritysten haku", "yrityshaut") Spelling variants: a few rows combine pure spelling/inflection variants of the SAME term (marked "also match …") so one search is not split across spellings. When such a combined row is reported, STATE in the report what it covers — e.g. "y-tunnus (kattaa: y-tunnus, ytunnus, yritystunnus)". Do NOT combine genuinely different words: quick ratio, current ratio and maksuvalmius are three separate rows, as are luottoriski, riskiluokitus and riskiraportti. Note on contains (no word boundaries): short terms can over-match compounds — e.g. "tulos" also matches "tuloslaskelma", "ebit" also matches "ebitda", "tase" also matches "tasearvo". Where this materially double-counts, note it; it is a minor, unavoidable limitation without regex. The distinction between liikevaihto, liikevoitto, käyttökate, ROE, ROA, and omavaraisuusaste must survive into the report. They imply different user intents and different SEO actions. Split generic vs company+modifier per term — a raw contains total mixes BOTH the bare generic ("liikevaihto") and the company+modifier long tail ("nokia liikevaihto", "kone liikevaihto", …). Report them as TWO separate rows, not one mixed sum: * generic value = query EQUALS the bare term (e.g. equals "liikevaihto"), plus known generic phrasings where enumerable (e.g. equals "yrityksen liikevaihto"); * company+modifier value ≈ contains(tokens) − generic(equals) − brand. Example: contains "liikevaihto" = 101, equals "liikevaihto" = 85 → [yritys] liikevaihto ≈ 101 − 85 = 16. Caveats to state: (a) the residual still includes other generic multi-word phrasings containing the term (e.g. "mikä on liikevaihto"), so it slightly OVERstates company+modifier — subtract known generic phrasings where you can, and label it an approximation; (b) rare per-company queries are anonymized, so the residual is a visible-part LOWER bound, not the true company+modifier total (the anonymized tail is captured only in aggregate by the invisible-tail estimate). Pure company-name traffic (approx, without top-N): run ONE aggregated Search Console call that excludes the brand AND all modifier tokens, by chaining a notContains for EACH token in the same call (the tool-supported equivalent of an exclude-union). Exclude these tokens: luottoriski, riskiluokitus, riskiraportti, luottoluokitus, rating, luottotiedot, luottotieto, luottokelpoisuus, maksuhäiriö, verovelka, ulosotto, perintä, konkurssi, saneeraus, taloustiedot, tilinpäätös, liikevaihto, liikevoitto, käyttökate, tulos, roe, roa, omavaraisuusaste, quick ratio, current ratio, maksuvalmius, y-tunnus, ytunnus, yritystunnus, tase, taseen loppusumma, tuloslaskelma, ebitda, ebit, roi, roce, yrityshaku, yritysten haku, yrityshaut, luottoriskit The remaining aggregate clicks are approximately searches that are only a company name, plus small noise and any modifier not listed. Label it "pelkät yritysnimihaut (arvio)"; it is a single server-side aggregate. Caveats: the anonymized tail is still excluded, and uncovered modifiers leak in. Keep the discovery pass to catch missing modifiers. Treat all term totals as indicators and this figure as approximate. If the MCP tool limits filtered queries, exact match, notContains, or row counts, state the limitation in the report and continue with the available Search Console data. Deeper fallback if server-side filtering (contains / equals / notContains) is unavailable entirely: do not leave the modifier section empty. Instead derive the categories by classifying the clicks>0 main pull (plus the impressions pull) from section 2 locally, matching each visible query to a single modifier by its tokens; queries with no modifier token count toward "pelkät yritysnimihaut". State that this local classification was used, that it is less precise than server-side filtering, and that it only covers the visible (non-anonymized) query rows. Keep the same narrow single-modifier granularity (liikevaihto separate from liikevoitto, etc.). ### 4. Invisible-tail estimate Search Console anonymizes rare queries. Rare queries are omitted whenever the query dimension is touched or a query filter is applied. Server-side category aggregation is more comparable and less biased than classifying top rows, but it does NOT guarantee recovery of the full anonymized long tail. Report the size of the invisible tail when it can be computed validly: * invisible tail ≈ total clicks (no query dimension, no filter) − sum of query-row clicks from the clicks>0 main pull To compute it validly, you MUST actually fetch two things for the SAME period: * total clicks with NO query dimension and NO filter * the clicks>0 main query pull at a high rowLimit (as many clicked-query rows as the API/MCP returns, not just a top-N list). Filtering to clicks>0 is correct here: zero-click rows would add 0 to the sum, so they do not affect the result — but a rowLimit cap that truncates CLICKED rows would, so note any cap. Only then subtract the query-row click sum from the total. Do NOT estimate the invisible tail from a top-N list. If the clicked-query pull was capped/truncated or only a top-N list is available, say the invisible tail cannot be computed exactly rather than fabricating a number. ### 5. Organic search pages Dimension: * page Metrics: * clicks * impressions * ctr * position Return top 20 rows by clicks for both periods. Compare the union of pages appearing in either Viikko A or Viikko B. Use Search Console page rows to connect organic search visibility to page types. Compare these against GA4 landing page data where possible. ### 6. Countries Dimension: * country Metrics: * clicks * impressions * ctr * position Return top 10 rows by clicks for both periods. Compare the union of countries appearing in either Viikko A or Viikko B. ### 7. Devices Dimension: * device Metrics: * clicks * impressions * ctr * position Return all available device rows for both periods. Compare the union of devices appearing in either Viikko A or Viikko B. Important: * Do not use GA4 searchTerm for organic search queries. * Use Search Console query data from Google search console MCP. * Search Console has no engagement metrics and GA4 has no query, so modifier-level engagement CANNOT be derived. Do NOT claim things like "ROE-haut sitoutuvat paremmin kuin luottoriski-haut". * Instead segment engagement by GA4 landing page PAGE TYPE (company pages vs. content pages such as /fi/luottoriski/, /fi/faq/, guide/pricing/onboarding pages). Use careful language such as "todennäköisesti", "laskeutumissivujen perusteella", and "viittaa siihen". * Search Console data may lag. If any requested date inside Viikko A or Viikko B is missing, state this clearly in the report. * Do not invent missing Search Console values. * If Search Console fails, include the exact error and continue with GA4-only reporting. * Do not recommend linking Search Console to GA4, because Search Console is now accessed directly via Google search console MCP. ## Data to retrieve from SERPRobot Call the SERPRobot `project_report` endpoint twice (one HTTP GET per window: Viikko A and Viikko B) and parse `report_data`. Match keywords across windows by `keyword_id`. From the returned data, derive: ### 1. Keyword position distribution (both windows) Bucket keywords by `latest_position` for each window: * top 3 (1–3) * top 10 (4–10) * 11–20 * 21–50 * 51–100 * ei sijoitusta (latest_position = null) Report counts and share of tracked keywords for Viikko A and Viikko B, plus the week-over-week change in each bucket. ### 2. Best and weakest tracked keywords (Viikko A) * Keywords ranking best in Viikko A (lowest latest_position), top 10, with their found_serp page. * Keywords with null or very weak positions, sorted by volume_local (then volume_global) descending — highest-volume gaps first. Note cpc_local / cmp_local where available to flag commercial value. ### 3. Movers (week-over-week only) * Use `latest_position` (each window's end-of-week standing) as the single position measure. Compare each keyword's Viikko A `latest_position` against its Viikko B `latest_position`, matched by `keyword_id`. Report the largest improvements and largest declines. Skip pairs where either side is null and state how many were comparable. * Do NOT compute or report the within-window `change` (earliest→latest) as a separate movers list. Because Viikko A and Viikko B are adjacent windows, within-window change is a near-duplicate of the week-over-week change and has caused misreadings. At most mention `change` as a one-line intra-week volatility note — never as a second list. * If almost all keywords have null positions on one side (fresh/thin history), state this plainly instead of forcing a list. ### 4. Search volume and commercial context * Highest-volume target keywords (volume_local) and their Viikko A positions. * Where available, note cpc_local and cmp_local to highlight high-value, high-competition gaps relevant to credit risk, company credit reports, risk assessment, and company valuation tools. ### 5. Freshness * Earliest and latest `updated` timestamp across keywords, so the reader knows how current the checks are. Use only real values from the API. If a field is null or "-", treat it as missing; do not fill it in. SERPRobot is useful for tracked strategic keywords and content gaps, but do not treat it as the complete picture of organic traffic. Search Console long-tail category aggregation is the better source for understanding the site's real long-tail traffic model. ## Analysis rules Compare Viikko A against Viikko B. For every comparison, calculate: * absolute change * percentage change, when mathematically valid Handle zero values safely: * if previous value is 0 and current value is greater than 0, write "uusi / prosenttivertailu ei mahdollinen" * if both values are 0, write "ei muutosta" * never divide by zero For engagementRate and CTR: * show as percentage * compare also in percentage points For averageSessionDuration: * show in seconds or minutes and seconds * compare absolute change and percentage change when valid For Search Console position: * lower position is better * write decreases in position number as "sijoitus parani" * write increases in position number as "sijoitus heikkeni" For SERPRobot rank tracking: * lower position is better; use the same "sijoitus parani" / "sijoitus heikkeni" language as Search Console position * the week comparison uses each keyword's `latest_position` (the window's end-of-week standing): compare Viikko A `latest_position` against Viikko B `latest_position` matched by `keyword_id`. If A is lower than B = "sijoitus parani", higher = "sijoitus heikkeni"; skip pairs where either side is null and say so * position is a STATE, not a flow: it cannot be summed over a week the way clicks are, and SERPRobot gives no weekly average — only earliest_position / latest_position / change per window. Use latest_position as the window's value. * do NOT use the within-window `change` (earliest→latest) as a separate movement measure — with adjacent windows it is a near-duplicate of the week-over-week change. At most a one-line intra-week volatility note. * the `updated` timestamp is when SERPRobot last refreshed a keyword (its own schedule), NOT the data window — do not present the `updated` range as the comparison period; the comparison periods are the Viikko A / Viikko B window ends. * treat null position and "-" volume as missing, not as zero; never fabricate historical values For weekly summaries: * use totals for count metrics such as users, sessions, pageviews, eventCount, keyEvents, clicks, and impressions * use returned period-level rates and averages when available * if you must calculate a rate or average from daily rows, state the calculation basis briefly * do not average percentages without weighting when totals are available For long-tail query reporting: * use category-filtered Search Console aggregates for comparable week-over-week trend * use exact query rows for SEO targeting and modifier discovery * do not infer query-level or modifier-level engagement from GA4 * keep informational intent vs. risk-aware/commercial intent central in interpretation * do not treat approximations such as "pelkät yritysnimihaut (arvio)" or invisible-tail estimates as exact facts ## Report format Write the report in Finnish. Keep it concise, action-oriented, and structured with short sections and bullet points. Avoid overly long tables. Use inline comparisons where possible, for example: "Käyttäjät: 1 234 → 1 456 (+222, +18,0 %)." Avoid letting the Search Console long-tail section make the email too long. If term rows are many, show the most important rows and summarize the rest. ## Required sections ### 1. Yhteenveto Write one concise paragraph: * kasvoiko vai laskiko liikenne viikkotasolla? * kasvoiko vai laskiko orgaaninen haku? * miltä seurattujen avainsanojen sijoitukset näyttävät (SERPRobot), jos dataa on saatavilla? * mikä oli tärkein syy? * mikä on tärkein toimenpide? In the summary, state explicitly whether the week's organic growth appears to come mainly from: * generic head terms * tracked SERPRobot keywords * long-tail company-name searches * long-tail company modifier searches * a mix of the above If long-tail company searches are a major driver, say so clearly and avoid framing generic keyword rankings as the main story unless the data supports it. ### 2. Ajanjaksot ja datan saatavuus Include: * Viikko A date range * Viikko B date range * GA4 data status * Search Console data status * SERPRobot data status for both windows (success/failure, and data freshness as the `updated` range) * whether the clicks>0 query pull (rowLimit 1000) succeeded, and the returned row count / whether it was capped * whether category-filtered Search Console queries succeeded (contains / equals / notContains) * whether the invisible-tail estimate could be computed * any missing Search Console dates inside either period * any failed queries or unavailable sections ### 3. Liikenne päämetriikoin Compare both weeks side by side for: * activeUsers * totalUsers * newUsers * sessions * engagedSessions * engagementRate * averageSessionDuration * screenPageViews * eventCount * keyEvents Show absolute values and changes. ### 4. Search Console -yleiskuva Compare both weeks for: * clicks * impressions * CTR * average position Explain whether organic visibility improved or worsened and whether the change was mainly driven by impressions, CTR, or ranking position. Also state, if computable: * total clicks without query dimension * sum of visible query-row clicks from the clicks>0 main pull * invisible-tail estimate ### 5. Uudet vs. palaavat käyttäjät Show the new/returning split for both weeks. Answer: * rakentuuko sivustolle palaavaa yleisöä? * vai perustuuko liikenne lähinnä uusiin kävijöihin? ### 6. Kanavat ja lähteet Show: * top channels by sessions for both weeks * week-over-week changes * top 5 source/mediums by sessions * which channels are most promising based on growth and/or engagement ### 7. Orgaaniset hakusanat ja pitkän hännän intentit Using Search Console data from Google search console MCP, structure this section as follows. #### 7.1 Top-hakusanat ja discovery Briefly list the top queries and label each as: * brändi * geneerinen päätermi * yritysnimi * yritysnimi + modifier * muu / epäselvä Use the clicks>0 main pull (plus the ~100-row impressions pull) to identify: * largest click increases and declines * high impressions but weak CTR * improving or worsening average position * exact company+modifier SEO targets with impressions but weak position or weak CTR Keep this concise. The exact rows are for SEO targeting and discovery, not the primary aggregation source. ALWAYS produce an explicit, clearly labelled output: "Uudet liitännäissanat (ei vielä modifier-listassa)". Scan the clicks>0 main pull (plus the ~100-row impressions pull) for modifier tokens that are NOT in the single-modifier category list above (e.g. "luottoraja", "toimitusjohtaja", "arvonlisävero", "omistajat", "hallitus", "osoite"). For each candidate modifier, report the approximate combined clicks/impressions across companies and 1–2 example queries, and recommend whether to add it to the modifier list next run. This is a required deliverable every run: if no meaningful new modifiers appear, state "ei uusia merkittäviä liitännäissanoja" explicitly rather than omitting the item. Note that these uncovered modifiers otherwise leak into the "pelkät yritysnimihaut (arvio)" figure, so flagging them also improves that estimate. Persistence (staging): before listing, read the `modifierit` sheet tab (see "## Muisti (Google Sheets)") and exclude modifiers already present there (active or candidate) — for those, note they are already proposed/active rather than re-proposing them. For each genuinely NEW modifier, APPEND a row to `modifierit` with status=candidate, first_seen=today, plus the approximate volume and 1–2 example queries. Only append candidate rows; never set status=active — promotion is a human step. This is what stops the same recommendation from repeating every run. #### 7.2 Modifierit ja liitännäishaut kategorioittain Present this as ONE table, one row per search term, with columns in this order: Hakutermi | Näytöt A | Klikit A | CTR A | Sijoitus A (GSC) | Sijoitus A (SERP) | Näytöt B | Klikit B | CTR B | Sijoitus B (GSC) | Sijoitus B (SERP) | Volyymi (global) Column sources and rules: * Näytöt / Klikit / CTR / Sijoitus (GSC) come from Search Console and describe OUR realized visibility. Sijoitus (GSC) = impression-weighted average position; always fill it when impressions > 0. * Sijoitus (SERP) = SERPRobot latest_position for that exact term (A column = Viikko A window, B column = Viikko B window). Use two DISTINCT empty markers and keep them separate: * "–" = no SERPRobot data / not applicable — the term is not a tracked SERPRobot keyword, or it is an [yritys]+modifier row (SERPRobot cannot aggregate those). * "ei sij." = the term IS tracked but SERPRobot returned no ranking (latest_position = null; ranks outside the tracked range). This is a real signal — demand may exist but we do not rank — NOT missing data. * Sijoitus (GSC): use "–" when the term had no impressions in that window (no GSC position exists then). * Volyymi (global) = SERPRobot volume_global for the term — ONE column, not per-week, since search volume is a slow monthly figure. Use volume_global for ALL volume figures. Use "–" when the term is not a tracked keyword or its volume is "-". * Keep GSC and SERPRobot in their own columns — GSC = how often WE appeared and where; SERPRobot Volyymi = market demand, SERPRobot Sijoitus = our rank for the exact term. Different questions; never merge them. Include a short legend (1–2 sentences) directly under the table so the reader can interpret it: * Sijoitus (GSC) = näyttökertapainotettu keskisijoitus kaikista hauista joilla sivumme näkyi (toteutunut näkyvyys); Sijoitus (SERP) = SERPRobotin aktiivinen sijoitus juuri kyseiselle hakusanalle, mitattu vaikkei termiä oikeasti haettaisi. * "–" = ei SERPRobot-dataa (termiä ei seurata, tai kyseessä [yritys]+modifier-rivi jota ei voi aggregoida); "ei sij." = termiä seurataan, mutta sillä ei ole sijoitusta seuratulla alueella (kysyntää voi olla, mutta emme rankkaudu). Rows — split each modifier as defined in "Data to retrieve from Search Console → 3": * the brand ("luottoriskit") on its OWN row * per modifier: a geneerinen row (equals) AND a [yritys]+modifier row (contains − equals − brand) Ordering: sort the term rows by Klikit A descending, then Näytöt A descending. EXCEPTION: always place "pelkät yritysnimihaut (arvio)" as the LAST row and exclude it from the sort — its very large click count would otherwise force it to the top, and it is a summary aggregate, not a single term. Include the narrow modifiers separately, especially: * luottoriski * luottoluokitus * luottotiedot * maksuhäiriö * verovelka * konkurssi * tilinpäätös * liikevaihto * liikevoitto * käyttökate * tulos * roe * roa * omavaraisuusaste * maksuvalmius * y-tunnus Also report below the table: * invisible-tail estimate, if computable * any approximation caveats (brand excluded via notContains; the contains − equals residual slightly overstates [yritys]+modifier; anonymized tail excluded) (The generic head figures and "pelkät yritysnimihaut (arvio)" are already rows IN the table — the geneerinen row per modifier, and the arvio pinned last — so do not repeat them separately.) Answer explicitly: 1. Which modifiers brought the most organic arrivals? 2. Which exact terms look like the clearest SEO targets? 3. Which modifiers look informational vs. closer to risk-aware or commercial/report intent? 4. Is the main organic story generic head terms, tracked keywords, pure company-name searches, company+modifier searches, or a mix? Do not claim exact company+modifier totals unless the filters make the split exact. Otherwise describe term values as indicators. #### 7.3 Sitoutuminen sivutyypeittäin, ei modifier-tasolla Search Console has no engagement and GA4 has no query, so modifier-level engagement CANNOT be derived. Do NOT claim things like: * "ROE-haut sitoutuvat paremmin kuin luottoriski-haut" * "liikevaihto-haut konvertoivat heikosti" * "luottoluokitus-haut ostavat paremmin" Instead segment GA4 landing-page engagement by PAGE TYPE: * company pages * /fi/luottoriski/ * /fi/faq/ and FAQ/model explanation pages * guide/support pages * pricing/onboarding pages Report page-type statements only, for example: * "yrityssivuille tuleva long-tail-liikenne sitoutuu laskeutumissivujen perusteella näin" * "sisältösivut, kuten /fi/luottoriski/ ja /fi/faq/, näyttävät sitouttavan paremmin/heikommin kuin yrityssivut" Use careful language: "todennäköisesti", "laskeutumissivujen perusteella", "viittaa siihen". #### 7.4 Tulkinta Explain what users are likely trying to do and what it means for: * content priorities * internal links * company-page modules * CTA strategy * future modifier tracking Keep informational vs. risk-aware/commercial intent central. ### 8. Orgaanisen haun sivut Using Search Console data from Google search console MCP, show: * top pages by clicks in Viikko A * pages with the largest increase in clicks * pages with the largest decline in clicks * pages with high impressions but weak CTR * pages where average position improved or worsened Compare these against GA4 landing page data where possible. Pay special attention to page types: * company pages * /fi/luottoriski/ * /fi/faq/ and model explanation pages * pricing/onboarding/support pages ### 9. Seuratut avainsanat ja sijoitukset (SERPRobot) Use SERPRobot `project_report` data for Viikko A and Viikko B (project SuomiFinder, region www.google.fi). Show: * seurattujen avainsanojen määrä ja sijoitusjakauma (top 3, top 10, 11–20, 21–50, 51–100, ei sijoitusta) Viikko A:lle ja Viikko B:lle sekä muutos viikkojen välillä * parhaiten sijoittuvat seuratut avainsanat Viikko A:ssa (matalin latest_position) ja niiden found_serp-sivu * suurimman hakuvolyymin avainsanat, joilla ei ole sijoitusta tai sijoitus on heikko (prioriteettiaukot); huomioi cpc_local / cmp_local kaupallisen arvon merkkinä * suurimmat nousijat ja laskijat VIIKKOMUUTOKSENA: vertaa kunkin avainsanan Viikko A:n `latest_position` Viikko B:n `latest_position`iin (sama keyword_id, vain molemmilla sijoitus ≠ null). Kerro montako paria oli vertailukelpoisia. ÄLÄ raportoi erillistä "viikon sisäistä liikettä" (earliest→latest) — se on lähes sama luku kuin viikkomuutos (ikkunat ovat vierekkäiset) ja on aiemmin johtanut väärintulkintaan. * sijoitukset ovat kunkin viikon LOPUN tilannekuvia — kerro vertailukohtina ikkunoiden loppupäivät (Viikko B:n ja Viikko A:n viimeiset päivät), ei `updated`-aikaväliä. * datan tuoreus: kerro `updated`-aikaväli VAIN erillisenä tuoreushuomiona (esim. "seuranta virkistetty viimeksi X"), ei vertailujaksona — hakusanan virkistysaika on eri asia kuin sen sijoituksen aikasarja. Käytä sääntöä (latest_position, viikkomuutos): Viikko A:n latest_position pienempi kuin Viikko B:n = "sijoitus parani", suurempi = "sijoitus heikkeni". Link the findings to Search Console and GA4: use `found_serp` to map tracked keywords to actual pages, and compare against Search Console top pages/queries and GA4 landing pages. Tracked keywords with null/weak SERPRobot positions and no Search Console visibility are content and visibility gaps, but do not let SERPRobot dominate the report if Search Console shows long-tail company searches as the real traffic driver. ### 10. Suosituimmat GA4-sivut Show top 10 pages by screenPageViews for Viikko A. Call out pages with: * high views but low engagement * strong engagement * potential content improvement opportunities Pay special attention to company pages, credit risk pages, credit report pages, company valuation pages, pricing pages, support pages, FAQ pages, guide pages, model explanation pages, and onboarding pages. ### 11. Laskeutumissivut Show top 10 landing pages by sessions with week-over-week comparison. Note pages with notably high or low engagement, especially pages that also appear in Search Console top pages. Segment the interpretation by page type: * company pages * credit risk / report pages * FAQ / model explanation / guide pages * pricing / onboarding / support pages Use this page-type segmentation for engagement interpretation. Do not infer modifier-level engagement. ### 12. Tapahtumat ja key eventit Show top events by count, compared week-over-week. Note meaningful changes in user behavior, especially events related to product interest, pricing, contact, onboarding, support, FAQ, request access, company credit reports, or valuation tools if visible in the data. If keyEvents are zero or very low, state that conversion tracking may be too sparse for strong conclusions. When CTA or purchase events do not scale with traffic, interpret this through intent. If organic growth is mainly informational company lookups, low report CTA growth is not automatically a CTA failure. Purchase measurement — do NOT equate checkout.stripe.com referral sessions with purchases. Both products go through the same checkout pipeline (create-checkout.js) → Stripe hosted checkout (checkout.stripe.com) → success_url returns to /fi/kiitos/, where the success event fires client-side: report=credit_risk → credit_report_purchase_success, report=ai_credit_risk → ai_report_purchase_success. These *_purchase_success events (with their transaction_id) are the reliable purchase signal. The checkout.stripe.com referral count in the source/medium data (section 6) is NOT a reliable proxy for purchases, and the two can move independently. GA4 sets a session's source/medium at session START, not from a mid-session referrer. When the user returns from Stripe: if the return continues the same session (usually within 30 min) the source stays the original (e.g. google/organic) and checkout.stripe.com is NOT logged as a separate referral session; a checkout.stripe.com / referral session appears only when the return starts a NEW session (source change or session timeout). So a drop in checkout.stripe.com referral sessions does NOT by itself mean purchases dropped — read purchase volume from the *_purchase_success events, and treat any referral-session correlation as loose context, not identity. ### 13. Viikkoraportoinnin luotettavuus Assess whether the site has enough weekly traffic for meaningful week-over-week reporting. Consider: * sessions * active users * engaged sessions * key events * Search Console clicks * visible query-row coverage vs. invisible-tail estimate * modifier-filter coverage * SERPRobot tracked-keyword coverage (how many keywords actually have a position vs. null) * volatility State whether weekly reporting is: * merkityksellistä * suuntaa antavaa mutta kohinaista * liian pientä luotettaviin viikkokohtaisiin päätelmiin ### 14. Kolme käytännön toimenpidettä Give exactly 3 specific, actionable recommendations to increase visitor volume and qualified interest in credit risk, company credit reports, risk assessment, and company valuation tools. Each recommendation must: * reference actual GA4, Search Console, or SERPRobot data from the report * mention a specific channel, source, page, query, tracked keyword, event, modifier, page type, or observed pattern * fit the business context of luottoriskit.fi * be concrete enough that someone can act on it immediately Additional requirements: * At least one recommendation MUST be based on aggregated long-tail modifier data, not a single generic keyword or a single company name. * At least one recommendation MUST distinguish informational intent from commercial/report intent. * Do not recommend stronger CTA placement generically. If recommending a CTA change, specify which intent category, page type, or modifier category supports it. * Do not give generic recommendations. ### 15. Yrityskohtaiset positiot (kiinteä paneeli) Track the fixed company panel's Google ranking over four rolling windows using Search Console page data. This section does NOT use Viikko A / Viikko B; it uses its own four windows defined below. Panel source: read the URL list from the Google Sheet defined in "## Yrityspaneeli (Google Sheets)" via Google Sheets MCP. Let N_total = number of panel URLs read. If the Sheet read fails, skip this section and state the exact error. Windows (all end 2 days before the local run date, to respect the Search Console lag). Let END = today − 2 days (the same date as Viikko A end): * 24h: start = END, end = END (the single most recent complete day) * 7d: start = END − 6, end = END * 28d: start = END − 27, end = END * 3kk: start = END − 89, end = END State these four date ranges in the section (d.m.yyyy). The windows are nested: 24h ⊂ 7d ⊂ 28d ⊂ 3kk. Data pull — ONE Search Console query PER WINDOW (four calls total), never one call per URL: * dimensions: ["page"] * metrics: clicks, impressions, ctr, position * filter: page contains "/fi/yritykset/" (the MCP does not support regex), then filter the response locally to the panel URL list. * rowLimit: high enough to include all panel pages (e.g. 25000). Only paginate with startRow if the returned row count equals the cap AND some panel pages are still missing. * Do NOT issue a separate call per URL. Do NOT echo the raw company-page rows into the report or the email; keep only the N_total panel matches. Match Search Console page rows to panel URLs by exact URL string. A panel URL with no matching row in a window = no impressions in that window; record its position as 0. This 0 is a coverage gap, NOT a rank of 0 and NOT a drop. Per-window panel metrics (recompute every run from that run's data; never carry a stored value): * N = number of panel URLs WITH impressions in that window, out of N_total. Report as N/N_total. * Balanced average position = unweighted mean of the per-page positions across the panel URLs WITH impressions in that window (each page weighted equally; do NOT impression-weight; exclude the 0 = no-impression pages). Additional aggregates: * Sijoitusjakauma (from the 3kk window, over pages with impressions): counts in buckets top 3 (1–3), top 10 (4–10), 11–20, 21–50, 51–100, and how many of N_total sit in top 10. * Suunta (28d vs 3kk, over pages with impressions in BOTH): count how many improved (lower 28d than 3kk), worsened, and stayed unchanged. * Top movers (per page, 3kk → 7d, over pages with impressions in BOTH windows): the 3 largest improvements and the 3 largest declines, each with before → after and the position delta. Output — write in Finnish prose in the report's style: state results directly, do NOT tell the reader to read the table. Structure the section as: Kokonaiskuva. One or two sentences: is the panel trending better or worse, is the movement within normal weekly noise, and is broad action needed? Then bullets: * Paneelin tasapainotettu keskisijoitus per window with N, e.g. "24h 8,6 (N=29/100), 7d 8,4 (N=66/100), 28d 8,7 (N=93/100), 3kk 9,1 (N=98/100)", plus a one-line read of 7d vs 3kk direction (fresher window better = slight improvement). * Näkyvyyskattavuus (N): coverage this run; how many of N_total got no impressions at all in 3kk (real visibility gaps worth checking — indexed? getting any search hits?); note that N falls in shorter windows because of search volume, not lost rank; a true coverage trend needs comparing the same window's N against previous runs. * Sijoitusjakauma (3kk): the bucket counts and the top-10 share. * Suunta (28d vs 3kk): improved / worsened / unchanged counts, and whether changes are mostly small (< 2 positions = normal organic noise) or structural. * Toimenpidetarve: whether there is a panel-level alarm or just a few individual movers (and any no-impression pages) to watch. Suurimmat nousijat (top 3, muutos 3kk → 7d): one line each — URL slug, before → after (+delta), short interpretation. Suurimmat laskijat (top 3, muutos 3kk → 7d): one line each, only for pages that have a meaningful 3kk impression baseline (skip pages with just a few 3kk impressions — too little volume to be a real mover). Do NOT treat a single window's 0 impressions — least of all 24h or 7d — as a technical alarm: low-volume company pages are simply not searched every day, so 0 there is normal volume variation, not a lost snippet. A faller is worth flagging as a possible technical/content issue (lost snippet, deindexing, cannibalisation) ONLY if it has effectively vanished across the LONGEST cumulative window as well (3kk at or near 0) despite having had 3kk impressions before (the 28d window or a prior `viikkoloki` row shows it did) — i.e. a sustained, cross-window disappearance, not a single fresh-day gap. Even then, first rule out the benign cause: an exact panel-URL string mismatch (trailing slash, slug or canonical change) that breaks the match and shows a phantom 0 across all four windows. If no faller clears that bar, say so plainly (e.g. "ei yksittäisiä hälyttäviä laskijoita — liikkeet normaalia volyymikohinaa") instead of manufacturing a red flag. Tärkein toimenpide: one short paragraph tying it together — whether the overall picture is fine, which individual pages to check, and any winners worth learning from. Menetelmähuomiot: 0 = no impressions in that window (not a rank drop); windows are nested (24h ⊂ 7d ⊂ 28d ⊂ 3kk) so they are not read as separate periods, and short-window small N is noisy; the average is balanced (each page equal weight) and recomputed every run; the page-dimension position is the impression-weighted average across all queries a page appeared for (for company pages this ≈ the company-name search); a panel URL showing 0 across ALL four windows is almost always an exact-URL match miss (trailing slash, slug or canonical change), NOT deindexing — verify the URL string before ever calling it a technical problem. Then a per-URL table with columns: URL, 24h, 7d, 28d, 3kk (0 = no impressions), ordered by the freshest position (7d, then 28d) best to worst. Include all N_total rows. ### 16. Muistin seuranta ja päivitys (Google Sheets) Use the `viikkoloki` and `modifierit` tabs (see "## Muisti (Google Sheets)") to close the loop across runs. Report (include briefly in the email): * Seuranta: read the last 2–3 `viikkoloki` rows and state whether items flagged in earlier runs changed this run (e.g. "viime ajossa merkitty AI-checkout-romahdus — palautuiko?"). If nothing was open, say "ei avoimia seurattavia edellisistä ajoista". * Uudet liitännäissanat: restate how many new candidate modifiers were staged this run (from section 7.1), or "ei uusia". Writes (perform AFTER the report text is composed, regardless of whether the email send later succeeds; never edit or overwrite existing rows): 1. Append candidate rows to `modifierit` for any genuinely new modifiers found in section 7.1 (status=candidate, first_seen=today). Skip modifiers already present (active or candidate). 2. Append exactly ONE new row to `viikkoloki` for this run: ajopvm (today), jakso (Viikko A range), compact avainluvut, condensed havainnot_ja_suositukset (including the 3 recommendations from section 14), seuranta_edellisiin, and avoimet_seurattavat to watch next run. If a sheet write fails, state the exact error in this section and continue; the email must still be sent. ## Email delivery Once the report is written, send it through Email report MCP using the send_report_email tool. Use these parameters: * To: [excel@valuatum.com](mailto:excel@valuatum.com) * Subject: Luottoriskit.fi GA4 + Search Console -viikkoraportti [VIIKKO A START]–[VIIKKO A END] Replace [VIIKKO A START]–[VIIKKO A END] with the actual Viikko A date range, for example: 2.6.2025–8.6.2025 Body: * the full report as clean, readable HTML * use standard HTML tags only: * h2 * h3 * p * ul * li * strong * table * thead * tbody * tr * th * td * no inline CSS * include both date ranges at the top so the reader knows what period is covered * include data availability status near the top, covering GA4, Search Console, and SERPRobot ### Email body handling and preflight validation Before calling the Email report MCP send_report_email tool, create one final variable named `final_report_html`. `final_report_html` must contain the complete, literal, final HTML string of the report that should appear in the email body. Never pass any of the following as the email HTML body: * a local file path, such as `/tmp/.../report.html` * a scratchpad path * a filename ending in `.html` * a placeholder such as `PLACEHOLDER_WILL_REPLACE` * a template that still contains unreplaced placeholders * a reference to where the report is stored * a summary of the report instead of the full report HTML If the report was first written to a file, read the file contents first and assign the actual file contents to `final_report_html`. Do not pass the file path to the email tool. Before sending, perform this preflight validation: 1. `final_report_html` must be a string containing the final report HTML. 2. `final_report_html` must not contain `PLACEHOLDER`, `WILL_REPLACE`, `TODO`, `/tmp/`, `scratchpad`, or an unreplaced template token. 3. `final_report_html` must not be a filesystem path and must not end with `.html`. 4. `final_report_html` must contain the report title: `Luottoriskit.fi — GA4 + Search Console + SERPRobot -viikkoraportti`. 5. `final_report_html` must contain both Viikko A and Viikko B date ranges. 6. `final_report_html` must contain real report sections and at least one metric, table, or bullet list derived from GA4, Search Console, SERPRobot, or an explicitly stated data-source error. 7. `final_report_html` must use only the allowed HTML tags listed above. 8. `final_report_html` must be long enough to plausibly contain the full report. If it is extremely short, treat it as invalid and do not send. Only after all checks pass, call Email report MCP send_report_email with the full literal value of `final_report_html` as the HTML body. If any preflight validation check fails, do not send the email. Instead, fix the HTML body first, then run the validation again. Send the email only once the validated `final_report_html` contains the complete final report. ## Constraints * Use only Google analytics MCP, Google search console MCP, Google Sheets MCP, and Email report MCP, plus the SERPRobot `project_report` endpoint defined above (HTTP GET only, one call per comparison window) * Do not browse the web, except for the one allowed SERPRobot endpoint * Do not search remote connectors * Do not use any other MCP tools * Call the SERPRobot `project_report` endpoint at most twice per run (once for Viikko A, once for Viikko B), and never print the SERPROBOT_API_KEY in the report, email, or chat * Do not ask for Google Cloud project ID or for the SERPROBOT_API_KEY during the report run * Assume Google analytics MCP, Google search console MCP, Email report MCP, and the SERPRobot API key are already configured correctly * Do not fabricate data * If a query fails, note it clearly in the report and skip only that section * The report must be sent even if some data sections are incomplete * If email sending fails, state the failure clearly and output the full generated HTML report in the chat as fallback * Never call Email report MCP send_report_email with a file path, scratchpad path, placeholder, or incomplete template as the HTML body. The email body must always be the complete literal final HTML report string after preflight validation.