★ Community Edition ★Price: Free



The Automation Wing

IndexNow Submitter: an n8n flow that pings the engines the moment a page ships

An importable n8n flow that watches your sitemap, spots new or changed URLs and pings IndexNow so Bing and Yandex hear about a page the moment it ships.

Google finds out about a new page from Search Console or a crawl. Bing and Yandex, and everything else that speaks the protocol, can find out the moment you tell them, because IndexNow is a single API call, not a queue you wait in. One ping covers every participating engine at once. Most sites never send it, not because it is hard, but because nobody wired it up. This flow wires it up.

It polls your sitemap on a schedule, works out which URLs are new or have a changed lastmod since the last run, and pings the shared IndexNow endpoint with just those URLs. It reads the sitemap and it notifies an external API; it changes nothing on the site itself.

Before you start

  • IndexNow requires a key file: a plain text file containing your key, served at the domain root, for example https://yourdomain.com/{key}.txt. Generate a key (any hex string will do) and publish the file before your first run, or every ping will fail verification.
  • This complements, it does not replace, a publish-time ping from your own CMS. A sitemap-based sweep catches anything that ships outside that code path, imports, bulk edits, a platform migration, or any CMS you have not instrumented directly.
  • The diff state lives in the workflow's own static data. Re-importing or duplicating the workflow resets it, so the next run treats every sitemap URL as new.

The flow

JSON
{
  "name": "IndexNow Submitter",
  "nodes": [
    {
      "parameters": {
        "rule": { "interval": [ { "field": "hours", "hoursInterval": 1 } ] }
      },
      "id": "1",
      "name": "Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [220, 300]
    },
    {
      "parameters": {
        "values": {
          "string": [
            { "name": "SITEMAP_URL", "value": "https://www.example.com/sitemap.xml" },
            { "name": "HOST", "value": "www.example.com" },
            { "name": "INDEXNOW_KEY", "value": "REPLACE_WITH_YOUR_KEY" },
            { "name": "KEY_LOCATION", "value": "https://www.example.com/REPLACE_WITH_YOUR_KEY.txt" }
          ]
        }
      },
      "id": "2",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [440, 300]
    },
    {
      "parameters": { "url": "={{$json.SITEMAP_URL}}", "options": {} },
      "id": "3",
      "name": "Get Sitemap",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [660, 300]
    },
    {
      "parameters": { "mode": "xmlToJson", "options": {} },
      "id": "4",
      "name": "Parse XML",
      "type": "n8n-nodes-base.xml",
      "typeVersion": 1,
      "position": [880, 300]
    },
    {
      "parameters": {
        "jsCode": "const urlset = $input.first().json.urlset || {};\nconst raw = urlset.url;\nconst entries = Array.isArray(raw) ? raw : (raw ? [raw] : []);\nreturn entries.map(e => ({ json: { loc: e.loc, lastmod: e.lastmod || '' } }));"
      },
      "id": "5",
      "name": "Flatten URLs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1100, 300]
    },
    {
      "parameters": {
        "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst seen = staticData.seen || {};\nconst current = $input.all().map(i => i.json);\nconst changed = current.filter(c => seen[c.loc] !== c.lastmod);\ncurrent.forEach(c => { seen[c.loc] = c.lastmod; });\nstaticData.seen = seen;\nreturn [{ json: { urls: changed.map(c => c.loc), count: changed.length } }];"
      },
      "id": "6",
      "name": "Diff Against Last Seen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1320, 300]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "typeValidation": "strict" },
          "conditions": [
            {
              "leftValue": "={{$json.count}}",
              "rightValue": 0,
              "operator": { "type": "number", "operation": "gt" }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "7",
      "name": "Has New URLs?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1540, 300]
    },
    {
      "parameters": {
        "url": "https://api.indexnow.org/indexnow",
        "method": "POST",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { host: $('Config').first().json.HOST, key: $('Config').first().json.INDEXNOW_KEY, keyLocation: $('Config').first().json.KEY_LOCATION, urlList: $json.urls } }}",
        "options": {}
      },
      "id": "8",
      "name": "Ping IndexNow",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1760, 220],
      "executeOnce": true
    },
    {
      "parameters": {},
      "id": "9",
      "name": "Log Result",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [1980, 220]
    }
  ],
  "connections": {
    "Every Hour": { "main": [[{ "node": "Config", "type": "main", "index": 0 }]] },
    "Config": { "main": [[{ "node": "Get Sitemap", "type": "main", "index": 0 }]] },
    "Get Sitemap": { "main": [[{ "node": "Parse XML", "type": "main", "index": 0 }]] },
    "Parse XML": { "main": [[{ "node": "Flatten URLs", "type": "main", "index": 0 }]] },
    "Flatten URLs": { "main": [[{ "node": "Diff Against Last Seen", "type": "main", "index": 0 }]] },
    "Diff Against Last Seen": { "main": [[{ "node": "Has New URLs?", "type": "main", "index": 0 }]] },
    "Has New URLs?": { "main": [[{ "node": "Ping IndexNow", "type": "main", "index": 0 }], []] },
    "Ping IndexNow": { "main": [[{ "node": "Log Result", "type": "main", "index": 0 }]] }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Set it up

  1. Publish your IndexNow key file at the domain root, then import the JSON above into n8n.
  2. Open the Config node and set SITEMAP_URL, HOST, INDEXNOW_KEY and KEY_LOCATION to your own values.
  3. Run the workflow once by hand. The first run treats every sitemap URL as changed, since nothing has been seen yet, so expect one large ping on that first execution.
  4. Activate it on the hourly schedule, or loosen it to every few hours on a low-churn site.

How to read the output

"Ping IndexNow" is set to run once per execution, not once per URL, because the whole point is a single call carrying the full urlList array; IndexNow accepts up to 10,000 URLs in one request. A 200 response means the engines have been notified, not that they have crawled or indexed anything yet, IndexNow is a discovery signal, not a guarantee. If "Has New URLs?" routes to the empty branch, nothing changed since the last run and no call is made, which is the expected steady state most hours.

One honest limitation: this only catches what the sitemap reflects. A page that never makes it into the sitemap, or a CMS that regenerates the sitemap on a delay, will not trigger a ping until the sitemap itself updates. If your own publish flow can call the IndexNow endpoint directly at the moment of publish, that is faster than this sweep; treat this flow as the safety net that catches everything else.

  • IndexNow
  • Indexing
  • n8n