URL: /capabilities/get-indexed/index-the-site · Id: `page:/capabilities/get-indexed/index-the-site`

# Index the site

> Your domain in, each key page’s index state out, read daily from Google and Bing until it is indexed or names why not.

Your domain in, each key page’s index state out, read daily from Google and Bing until it is indexed or names why not.

```text title="Run it · Ask"
Get every page on leaseline.app indexed by Google and Bing, spend up to $2, and show me the plan first.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Site.index({ goal: 'Get every page on leaseline.app indexed by Google and Bing, spend up to $2, and show me the plan first.', budget: { amount: 2, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "Free",
  "sample": {
    "page": "https://leaseline.app/rent-ledger-template",
    "state": "crawled",
    "reason": "Crawled - currently not indexed",
    "since": "2026-10-25"
  }
}
```

- **Kind:** Workflow
- **Price:** Free
- **Your time:** 5 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:content-marketing`](https://startupbuilder.dev/workflows/processes/content-marketing)

## What you get
- **1 site** · Your domain in, each key page’s index state out, read daily from Google and Bing until it is indexed or names why not. · [`noun:site`](https://startupbuilder.dev/business/nouns/site)
- `engines` · Google and Bing verified or pending, and whether IndexNow is set.
- `sitemap` · URLs submitted against URLs indexed: the first number a new site can read, weeks before any clicks.
- `pages[].state` · `discovered`, `crawled`, `indexed` or `excluded`, with the date it got there.
- `pages[].reason` · The engine’s own reason for a page that is not indexed: noindex, canonical elsewhere, soft 404, or crawled and not indexed.

**Connect once:** [Google Search Console](/code/integrations/google-search-console), [Bing Webmaster](/code/integrations/bing-webmaster). **How long:** Setup in about a minute; then it waits days to weeks on the engines, and the waits cost nothing.

## Trigger
It runs once when you connect the domain; every page you publish later is pinged as it goes live.

```ts
on.Domain.connected((domain, $) => $.Site.index({ site: domain.hostname }))
```

## Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.

| Step | Verb | Returns | Price | Approval |
| --- | --- | --- | --- | --- |
| 1. Connect the domain to Google and Bing | `Domain.connect` | Both properties verified by DNS, the sitemap submitted to each and the IndexNow key set, as effects of the connect | Free | Runs on its own |
| 2. Read index state on each key page, daily | [`Domain.verify`](/code/data/domains/verify) | Each key page’s state and the engine’s reason, read from your own Search Console within its 2,000 a day | Free | Runs on its own |
| 3. Press Request indexing on stuck pages | [`Integration.call`](/code/integrations/calls/call) | The request pressed in Search Console, read back from its page; off unless `pressRequest` is set | Free | Runs on its own |
| 4. Hand stragglers to the weekly readout | `Collection.query` | Pages still not indexed after `until`, with their reasons, added to Search readout | Free | Runs on its own |

Calls: `Domain.connect`, then `Domain.verify`, then `Integration.call`, then `Collection.query`.

Free, each time it runs, capped at $2.00.

## Example: give it a goal
> Get every page on leaseline.app indexed by Google and Bing, spend up to $2, and show me the plan first.

The plan it shows runs 4 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| The Search Console connection needs signing in again | It stops before any read with the fix: “Reconnect Search Console: `npx startup-builder connect google`”. Nothing is billed. |
| A page reads `excluded` | It records the engine’s reason and opens a task for the site owner: an excluded page needs a fix, not money, so goal mode never spends on it. |
| The 2,000-a-day inspection quota is spent | The rest wait until the quota resets (`quota-reached`, with `resetsAt`) and are read first the next day. |
| You call it again on the same site | It re-reads state and never re-adds a property: it is idempotent per site. |

Each step retries three times, then falls through to the next route; one item's failure lands in `skipped` with its reason, never failing the call. Before a step would cross `maxCost` it stops and returns what it has, with a `resume` link. Each step's idempotency key is the call's key plus the step and the item: `leaseline-site-index-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Press Request indexing on any page stuck for a week. | [`Site.index`](/capabilities/get-indexed/index-the-site) |
| Tell me the day a page drops out of Google. | [`Monitor.create`](/code/data/monitors/create) |
| Check every page in the sitemap, not just the key ones. | [`Domain.verify`](/code/data/domains/verify) |

```ts
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'site-index',
  verb: 'Site.index',
  input: { site: 'string', keyPages: 'string[]?', pressRequest: 'boolean' },
  maxCost: 2,
  execute: async (ctx, { site, keyPages, pressRequest = false, until = 28 }) => {
    await ctx.step('connect', () => $.Domain.connect({ hostname: site, engines: ['google', 'bing'], indexNow: true }))   // verify, sitemap, IndexNow
    const pages = keyPages ?? (await ctx.step('pages', () => $.Collection.query({ from: 'page', where: `site = '${site}'`, sort: '-links', limit: 20 }))).map((p) => p.url)
    for (let day = 0; day < until; day++) {
      const state = await ctx.step(`state-${day}`, () => $.Domain.verify({ hostname: site, pages }))                 // free on your own account
      if (state.pages.every((p) => p.state === 'indexed' || p.state === 'excluded')) return state
      const stuck = state.pages.filter((p) => p.state === 'crawled' && p.days >= 7)
      if (pressRequest && stuck.length) await ctx.step(`request-${day}`, () => $.Integration.call({ integration: 'google-search-console', op: 'request-indexing', urls: stuck.map((p) => p.url), maxCost: 0.16 * stuck.length }))
      await ctx.sleep('1d')
    }
  },
})
```

Extend it with `extends: 'Site.index'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [Make your API easy for agents to find](/plays/agent-legibility), [Developer relations](/plays/developer-relations), [Free tools](/plays/free-tools), [Open source](/plays/open-source), [Programmatic SEO](/plays/programmatic-seo).

It serves [Get indexed](/capabilities/get-indexed), and runs in [Launch and search](/strategy/launch-and-search). It deepens [`process:content-marketing`](https://startupbuilder.dev/workflows/processes/content-marketing).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Site.index` |
| Guide approval | `limits` |
| Goal | Get every page on leaseline.app indexed by Google and Bing, spend up to $2, and show me the plan first. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | `on.Domain.connected((domain, $) => $.Site.index({ site: domain.hostname }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-site-index-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Connect the domain to Google and Bing | None | None. |
| 2. Read index state on each key page, daily | None | None. |
| 3. Press Request indexing on stuck pages | None | None. |
| 4. Hand stragglers to the weekly readout | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Connect the domain to Google and Bing | `code` | Code | Code reads the result back from where it landed | `measured` | `$0.site` |
| 2. Read index state on each key page, daily | `code` | Code | Code checks each result against its declared type | `measured` | `$0.site, $0.keyPages` |
| 3. Press Request indexing on stuck pages | `agentic` | The [Content lead](/capabilities/agents/content-lead) agent | Code reads the result back from where it landed | `measured` | `$2.stuck, $0.pressRequest` |
| 4. Hand stragglers to the weekly readout | `code` | Code | Code checks each result against its declared type | `measured` | `$2.pages, $0.until` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | [Sites and content](/code/apps/sites-and-content) |
| Nouns | [`noun:domain`](https://startupbuilder.dev/business/nouns/domain), [`noun:integration`](https://startupbuilder.dev/business/nouns/integration), [`noun:collection`](https://startupbuilder.dev/business/nouns/collection) |
| Events | `Domain.connected`, `Domain.verified`, `Integration.called`, `Collection.queried` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `site` | `Hostname` | Required | Your site. |
| `keyPages` | `string[]` | `home and the 19 most-linked pages` | The pages read daily. The sitemap still covers every page. |
| `pressRequest` | `boolean` | `false` | Press Request indexing on a page still `crawled` after 7 days, through the browser agent signed in as you. |
| `until` | `string` | `'28d'` | How long it reads before it hands the stragglers to the weekly Search readout. |
| `maxCost` | `number` | `2.00` | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a `resume` link. |

Emits `Site.indexed`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).