# Claude Code Routines for Data Collection: A Worked Example

> A Claude Code routine can run open-ended data collection on a schedule: Browserbase does the browsing, a pull request carries the data, a human approves.

Source: https://capitalandcompute.net/blog/claude-code-routines-data-collection/
Published: 2026-10-20
Updated: 2026-11-14

A Claude Code routine is a saved prompt, repository and cloud environment that Anthropic runs for you on a schedule or an API call. For open-ended data collection, one routine drives Browserbase cloud browsers, writes what it finds as files with provenance, and opens a pull request. A second routine reviews it, and a person approves the merge.

This is the setup one team is moving its data collection onto, written up as pseudo code so the pattern travels. What gets collected stays out of it, and so does the project. The shape is the point: who holds the work at each step, where the credentials live, and where a human has to sign off.

Anthropic launched routines in research preview on 14 April 2026, per its announcement [_Introducing routines in Claude Code_](https://claude.com/blog/introducing-routines-in-claude-code). The [routines documentation](https://code.claude.com/docs/en/routines) still carries the research-preview label as of October 2026, so limits and the API can change under you.

Key takeaways

* A routine is a full Claude Code cloud session with no permission prompts. It gets the whole harness for free: the agent loop, Bash, skills and hooks committed to the repo, and git.
* Browsing happens inside Browserbase, not inside the routine. The routine VM only needs to reach the Browserbase API, so its network allowlist stays tight while the browser goes anywhere.
* Store the Browserbase key as a network secret. Anthropic's proxy attaches it to requests for the hosts you list, so the key never sits inside the session.
* Routine pull requests carry your own GitHub identity, and GitHub does not let an author approve their own pull request. A teammate has to approve, which is the gate you want anyway.

## Why use a Claude Code routine instead of a scraper?

Because the job is open-ended. A scraper works when you know the sites, the page layout and the field you want. This collection work starts from a worklist of questions, and the answer can sit on a page nobody has visited yet: a PDF linked three clicks deep, a page behind a cookie wall, a table that moved since last quarter.

Writing a scraper per source would mean writing hundreds of them, and rewriting each one every time a site changed. An agent can read a page it has never seen, decide which link to follow, notice a wall, and record why it gave up. That judgement is the expensive part, and it is exactly what a model is for.

The obvious next step would be to build an agent: a loop, tool definitions, retries, a supervisor, somewhere to run it. The team started down that road with a sandbox agent on another framework. Then the question changed to: why rebuild what Claude Code already is? Claude Code is an agent harness that already has the loop, file and shell tools, skills, hooks, subagents and git. Routines add the two missing pieces, a clock and a cloud machine. (If you’re weighing that trade-off in general, [build vs buy an agent harness](/blog/build-vs-buy-agent-harness/) walks through it.)

So the collection agent is not a program the team maintains. It’s a prompt, a skill file in the repository, and a short browser wrapper.

## What does the pipeline look like?

Nine steps, five owners. The routine does the thinking, Browserbase does the browsing, GitHub and CI do the checking, a second routine reads the result, and a person decides.

__Who holds the work at each step of a scheduled data-collection routine__
| Step | Owner              | What happens                           |
| ---- | ------------------ | -------------------------------------- |
| 1    | Collection routine | Schedule or API fire starts a run      |
| 2    | Collection routine | Read worklist, pick N targets          |
| 3    | Browserbase        | Open session, load the page            |
| 4    | Browserbase        | Return bytes + replay link             |
| 5    | Collection routine | Hash, extract, write records           |
| 6    | GitHub + CI        | Push claude/ branch, open pull request |
| 7    | GitHub + CI        | Schema and provenance checks           |
| 8    | Review routine     | Re-fetch a sample, post a review       |
| 9    | Teammate           | Approve and merge (human sign-off)     |

The collection routine never merges its own work. Its output becomes a pull request, CI and a review routine check it, and only a teammate's approval lands it on main.Source: Capital & Compute, from Anthropic's routines and cloud environments documentation, checked 20 October 2026

Two properties fall out of that shape. Nothing the routine writes reaches main without a person. And every claim in the data carries the URL, the time and the hash of the bytes it came from, so the reviewer checks evidence instead of trusting the agent.

## How do you set up the routine?

A routine has four parts: the prompt, the repositories, the environment and the triggers. Create it at claude.ai/code/routines or with `/schedule` in the CLI. The documentation says all surfaces write to the same account.

### Keep the real prompt in the repository

This is the one design choice to steal even if you skip the rest. Routines live in your claude.ai account, not in git, so a prompt typed into the routine form has no history and no review. Keep the routine’s prompt down to a pointer, and put the instructions in a file that changes through pull requests like any other code:

```
<!-- Routine prompt, as saved in the routine form (pseudo) -->

You are the scheduled data-collection run.
Follow .claude/skills/collect/SKILL.md exactly. It is the task.
If the routine-fire-payload block names targets, collect only those.
Success = a pull request from a claude/collect-<date> branch, or a
one-line note in the run log saying the worklist was empty.
```

The skill file then carries the procedure. A trimmed version:

```
<!-- .claude/skills/collect/SKILL.md (pseudo) -->

1. Run `pnpm collect:worklist --limit 10`. Take the first 10 open rows.
2. For each row, find candidate addresses with search. Never read a
   value from a search snippet; snippets only tell you where to look.
3. Fetch each address with `pnpm collect:browse <url>`. Never fetch
   pages yourself with curl; the wrapper keeps the bytes and the hash.
4. Read the held text. Write one record per value, each with a quote.
5. A page that refuses you is a result, not a failure. Record
   outcome=refused with the status and stop retrying that host.
6. Run `pnpm collect:validate`. Fix what it reports, or drop the row.
7. Commit to a claude/collect-<date> branch and open a PR that lists
   rows closed, rows refused and rows skipped.
   Stop when 10 rows are done or 40 minutes have passed, whichever first.
```

Step 2 sounds fussy. It’s the rule that matters most. A search snippet is a summary someone else made, sometimes of an old version of the page. The data has to come from the bytes the wrapper held, or the provenance is fiction.

### Give it an environment with a narrow network

The routine runs in a [cloud environment](https://code.claude.com/docs/en/cloud-environments), which controls network access, variables and a setup script. The **Default** environment uses **Trusted** access, which allows only a list of package registries and common development hosts. Browserbase isn’t on it. Requests to other hosts fail with a `403` and `x-deny-reason: host_not_allowed`, per the routines documentation.

So create a dedicated environment:

```
# Cloud environment "collector" (pseudo; set in the claude.ai form)
network_access: custom
allowed_domains:
  - api.browserbase.com
  - '*.browserbase.com' # the CDP connect host from the session's connectUrl
include_default_package_list: true
network_secrets:
  - name: browserbase
    hosts: [api.browserbase.com]
    header: 'X-BB-API-Key: <stored once, never visible to the session>'
setup_script: |
  pnpm install --frozen-lockfile
```

Here’s the part that makes the design work. The pages being collected load inside Browserbase’s browser, on Browserbase’s network. The routine’s VM never talks to them. It only talks to the Browserbase API. So the agent can roam the open web while the machine running it can reach almost nothing.

The key goes in as a [network secret](https://code.claude.com/docs/en/cloud-environments): Anthropic’s agent proxy adds it to requests for the hosts you list after they leave the session’s VM, so the key never sits inside the session. Plain environment variables are readable by anyone who uses the environment, and the documentation says to prefer secrets for API keys. One practical catch: if the Browserbase SDK refuses to start without a key, pass it a placeholder and confirm in a first manual run that the proxy’s header authenticates the requests.

### Pick the triggers

Two triggers cover this job. A schedule runs the routine every six hours, and an API trigger lets anything else start a targeted run.

```
Schedule:  7 */6 * * *   # set with `/schedule update`; minimum interval is 1 hour
```

The docs warn that a run scheduled exactly on the hour can start several minutes late, so pick a few minutes past. The API trigger is a POST with a per-routine bearer token:

```
# Fire a targeted run (pseudo IDs; the beta header is from the docs)
curl -X POST https://api.anthropic.com/v1/claude_code/routines/$ROUTINE_ID/fire \
  -H "Authorization: Bearer $ROUTINE_TOKEN" \
  -H "anthropic-beta: experimental-cc-routine-2026-04-01" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"text": "Targets: rows 118, 204, 377. Re-check after a source update."}'
```

The `text` arrives wrapped in a `<routine-fire-payload>` block that labels it as untrusted data. The routine only acts on it if the saved prompt says to, which is why the prompt above names the block explicitly. Anyone holding the token can send text, so keep the instruction narrow: the payload may name targets, nothing more.

## How does the routine drive Browserbase?

Through one small wrapper the skill calls, not through a browser tool the model operates click by click. The wrapper creates a session, connects Playwright over CDP, keeps the server’s bytes, and always releases the session.

```
// scripts/collect/browse.ts (pseudo, trimmed)
import Browserbase from '@browserbasehq/sdk';
import { chromium } from 'playwright-core';
import { createHash } from 'node:crypto';

const bb = new Browserbase({ apiKey: process.env.BB_KEY ?? 'set-by-network-secret' });

export async function browse(url: string) {
  const session = await bb.sessions.create({
    browserSettings: { solveCaptchas: true },
    timeout: 120, // seconds; the session ends even if this process dies
  });
  try {
    const browser = await chromium.connectOverCDP(session.connectUrl);
    const page = browser.contexts()[0].pages()[0]; // the stealth-configured default page
    const res = await page.goto(url, { waitUntil: 'domcontentloaded' });
    const bytes = await res!.body(); // what the server sent, not the rendered DOM
    const sha256 = createHash('sha256').update(bytes).digest('hex');
    await saveBlob(sha256, bytes); // evidence/<sha256>, tracked in the repo
    return {
      url,
      status: res!.status(),
      sha256,
      fetchedAt: new Date().toISOString(),
      replay: `https://www.browserbase.com/sessions/${session.id}`,
      text: toText(bytes).slice(0, 20_000), // what the model reads
    };
  } finally {
    await bb.sessions.update(session.id, { status: 'REQUEST_RELEASE' });
  }
}
```

Four choices in there earn their lines:

* **`timeout` is a billing kill switch.** Browserbase bills by browser time. The [create-session API](https://docs.browserbase.com/reference/api/create-a-session) ends a session automatically after `timeout` seconds (60 minimum), so a crashed run can’t leave a browser running all night.
* **`REQUEST_RELEASE` in `finally`.** The [update-session API](https://docs.browserbase.com/reference/api/update-a-session) accepts one status, `REQUEST_RELEASE`, which ends the session early “to avoid unnecessary usage charges.”
* **Hash the response body, not the DOM.** The rendered DOM changes with every ad and timestamp. The server’s bytes are what you can re-fetch later and compare.
* **The model gets text and a replay link, not raw bytes.** A 4 MB PDF in the context window helps nobody. The replay link lets the human reviewer watch the session if a record looks wrong.

## What does a collected record look like?

Every value lands as a row that points back to its evidence. The validator refuses a row without a source, a hash or a quote.

```
{
  "id": "row-0377",
  "field": "list_price_monthly",
  "value": 45,
  "quote": "Standard plan: 45 per month, billed annually.",
  "source": {
    "url": "https://example.org/guidance.pdf",
    "fetched_at": "2026-10-20T06:07:41Z",
    "sha256": "9f2c…e41a",
    "outcome": "held",
    "replay": "https://www.browserbase.com/sessions/…"
  },
  "collected_by": "routine:collect",
  "run": "session_01…"
}
```

A refusal is a row too:

```
{
  "id": "row-0204",
  "source": {
    "url": "https://example.net/register",
    "outcome": "refused",
    "failure": "HTTP 403 to a stealth session from two regions; host blocks automated clients",
    "fetched_at": "2026-10-20T06:11:02Z"
  }
}
```

That second record matters more than it looks. Without it, the next run tries the same host, gets the same 403 and burns the same minutes. With it, the worklist skips the host until someone changes the approach. Sources are append-only: when a page changes, the new hash is recorded as drift next to the old one, and the old evidence is never overwritten.

## How do checks, review and approval work on the pull request?

The routine pushes to a `claude/`\-prefixed branch, which the docs say is the default, and opens a pull request. Three gates sit between that pull request and main.

**Gate 1: CI.** A required check runs the same validator the routine ran, plus a hash check on every new evidence file:

```
# .github/workflows/collect-check.yml (pseudo)
on:
  pull_request:
    branches: [main]
jobs:
  validate:
    if: startsWith(github.head_ref, 'claude/collect-')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install --frozen-lockfile
      - run: pnpm collect:validate --strict # schema, quote present, source present
      - run: pnpm collect:verify-hashes # sha256(evidence/<file>) == file name
      - run: pnpm collect:append-only # no existing source row edited or deleted
```

**Gate 2: a review routine.** A second routine with a GitHub trigger on `pull_request.opened`, filtered to head branches that start with `claude/collect-`, re-fetches a random sample of the new rows through the same wrapper and compares hashes and quotes. Its prompt is short:

```
<!-- Review routine prompt (pseudo) -->

A collection PR was opened. Pick 3 new rows at random. For each,
fetch source.url with `pnpm collect:browse`. Confirm the quote appears
in the fetched text and the value matches the quote. Post one review
comment listing each row as confirmed, changed or unreachable.
Do not push commits to this branch.
```

The review routine needs the [Claude GitHub App](https://github.com/apps/claude) installed on the repository. Webhook events are capped per routine and per account per hour, and the docs say events over the cap are dropped. So the review routine is a helper, never the only gate.

**Gate 3: a teammate.** This is where the identity rule does useful work. A routine acts as you: its commits and pull requests “carry your GitHub user,” per the routines documentation. And GitHub’s own [documentation on required reviews](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/approving-a-pull-request-with-required-reviews) is blunt: “Pull request authors cannot approve their own pull requests.” Neither routine can approve the collection PR, because both of them are its author. Turn on branch protection with one required approval and the required CI check, and the only path to main runs through another person.

The teammate reads the PR body (rows closed, refused, skipped), the review routine’s sample, and any row that looks odd, using the replay link. That’s a few minutes of judgement instead of an afternoon of browsing.

## What breaks, and which guardrails matter?

The pages are the attacker

A routine runs shell commands and calls every included connector without asking for permission. The collection routine reads pages written by strangers, including sites that actively block bots. Treat every fetched page as hostile input: give the routine no connectors it doesn’t need, a network that reaches only Browserbase and package registries, and no path to main without a human.

How the same collector fits into the wider workflow (local worktrees for trusted code, cloud sessions for untrusted pages, and the rules that stop a scheduled run failing silently) is in [the Claude Code autonomous workflow](/blog/claude-code-autonomous-workflow/).

The failure modes, roughly in the order the team expects to hit them:

| What breaks                      | What it looks like                             | Guardrail                                                                                        |
| -------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| A green run that did nothing     | The run list shows success, no PR appears      | The docs say green means “exited without an infrastructure error,” not success. Open the run     |
| Prompt injection from a page     | A page says “ignore your instructions and…”    | No connectors, narrow network, CI and human gate. The agent can be fooled; it can’t merge        |
| A host that blocks every session | Repeated 403s, minutes billed for nothing      | Record outcome: refused, skip the host on later runs                                             |
| A run that never ends            | Usage drains, Browserbase time accrues         | Row cap and time cap in the skill, timeout on every session                                      |
| Silent value drift               | Last quarter’s number, still on main           | Scheduled re-fetch of held sources; a changed hash opens a drift row, never an overwrite         |
| Hitting run limits               | A scheduled run waits, a fire returns an error | Few, larger runs. The announcement caps daily runs by plan; the docs add hourly caps per account |
| GitHub connection lapses         | Runs skip                                      | The docs say runs skip for up to 72 hours, then the routine turns itself off                     |

One more failure mode rarely gets written down: a routine runs on your personal claude.ai account. The docs say routines “are not shared with teammates.” If you leave, or your seat changes, the collection stops. Document the routine’s settings in the repository next to the skill so someone else can recreate it in ten minutes.

For the wider checklist of what to lock down in any agent harness, see the [agent harness checklist](/blog/agent-harness-checklist/).

## What does it cost to run?

Two meters run at once, and neither has a per-run price you can quote in advance.

**Claude usage.** Routines “draw down subscription usage the same way interactive sessions do,” per the documentation. There is no separate routine fee, but a long collection run eats into the same allowance your interactive sessions use. The April announcement also capped daily runs at 5 on Pro, 15 on Max and 25 on Team and Enterprise. If you’re unsure whether a subscription or API billing fits a workload like this, the [subscription vs API calculator](/ai-subscription-vs-api-calculator/) does the arithmetic.

**Browserbase time.** Browserbase’s [pricing page](https://www.browserbase.com/pricing), checked 19 October 2026:

| Plan      | Monthly price | Browser hours included | Concurrent browsers | Overage per browser hour | Captcha solving       |
| --------- | ------------- | ---------------------- | ------------------- | ------------------------ | --------------------- |
| Free      | $0            | 1                      | 3                   | n/a                      | No                    |
| Developer | $20           | 100                    | 25                  | $0.12                    | Yes, basic stealth    |
| Startup   | $99           | 500                    | 100                 | $0.10                    | Yes, basic stealth    |
| Scale     | Custom        | 500+                   | 250+                | Usage-based              | Yes, advanced stealth |

The Browserbase side is easy to estimate: runs per day × pages per run × average session seconds. Four runs a day, 10 pages each, at about 40 seconds a session comes to roughly 13 browser hours a month, inside the Developer plan’s 100\. The free plan’s single hour runs out in days, and the team’s sandbox hit Browserbase’s free-plan minutes limit early in testing. The Claude side depends on how much each page makes the model read and think, which is why the wrapper truncates text before the model sees it.

## How do you get started?

The order the team is rolling it out in, smallest risk first:

1. **Write the skill and the wrapper, then run them by hand.** Open a normal Claude Code session, say “follow .claude/skills/collect/SKILL.md for 2 rows,” and watch. Most fixes happen here, while you can still interrupt.
2. **Create the cloud environment.** Custom network with the Browserbase hosts, the key as a network secret, the setup script. Start one session in it by hand to confirm the browser connects.
3. **Add the CI check and branch protection** before any routine exists, so the first unattended pull request already meets the gate.
4. **Create the collection routine with no trigger, and use Run now.** Read the whole transcript of the first few runs. A green status proves nothing.
5. **Add the schedule**, then the review routine, then the API trigger, one at a time.

## Why not GitHub Actions, or a hosted agent?

Both work. The [Claude Code GitHub Action](https://code.claude.com/docs/en/github-actions) can run the same skill on a cron and open the same pull request. The trade-offs are where you pay and what you can see. Actions bills API tokens or a subscription token plus runner minutes, and a run leaves a log. A routine bills the subscription, and each run is a full session at claude.ai/code that you can open, read and continue by hand when something looks off. For a job where half the debugging is “why did it decide that?”, the session view wins. If the routine needs your private network rather than the public web, the same sessions can run on your own hardware, covered in [Claude Code self-hosted runner vs GitHub Actions](/blog/claude-code-self-hosted-runner-vs-github-actions/).

A hosted agent platform is the other route, and [Claude Agent SDK vs Managed Agents](/blog/claude-agent-sdk-vs-managed-agents/) compares those. They make sense when the agent isn’t a coding session at all. Here the output is files in a repository, reviewed as a pull request, so a coding agent with git built in is the shorter path. For where routines sit among the other ways to run Claude Code unattended, see [Workflow 6 in Claude Code agentic workflows](/blog/claude-code-agentic-workflows/). The [AI coding agents hub](/ai-coding-agents/) collects the rest of the harness series.

## Frequently asked questions

What is a Claude Code routine?

A routine is a saved Claude Code configuration (a prompt, one or more GitHub repositories, a cloud environment and optional connectors) that runs on Anthropic-managed cloud infrastructure when a trigger fires. Triggers can be a schedule, an HTTP POST to a per-routine API endpoint, or a GitHub pull request or release event. Routines are in research preview on Pro, Max, Team and Enterprise plans.

Can a Claude Code routine browse the web?

Yes, within its environment's network policy. The Default environment only allows a list of package registries and development hosts, so a routine that needs a browser either allows the target domains or, more safely, calls a cloud browser service such as Browserbase and allows only that service's API. The pages then load on the browser service's network, not the routine's.

How do you give a routine an API key safely?

Add it to the cloud environment as a network secret. Anthropic's agent proxy attaches the key to requests for the hosts you list after they leave the session VM, so the session never sees the key. Plain environment variables are readable by anyone who uses the environment.

Can a routine approve or merge its own pull request?

It should not be able to. Routine commits and pull requests carry your GitHub user, and GitHub does not allow a pull request author to approve their own pull request. With branch protection requiring one approval, a teammate has to approve before anything the routine wrote reaches main.

How often can a routine run?

The minimum schedule interval is one hour. The routines documentation also sets hourly caps: 100 scheduled runs per account, and 30 Run now or API fires per routine. The April 2026 announcement capped daily runs at 5 on Pro, 15 on Max and 25 on Team and Enterprise. All of these may change during the research preview.

## Sources

* Anthropic (2026). _Introducing routines in Claude Code_. Claude blog (vendor announcement). <https://claude.com/blog/introducing-routines-in-claude-code>
* Anthropic (2026). _Automate work with routines_. Claude Code documentation. <https://code.claude.com/docs/en/routines>
* Anthropic (2026). _Configure cloud environments_. Claude Code documentation. <https://code.claude.com/docs/en/cloud-environments>
* Anthropic (2026). _Claude Code GitHub Actions_. Claude Code documentation. <https://code.claude.com/docs/en/github-actions>
* Browserbase (2026). _Create a session_. Browserbase API reference (vendor documentation). <https://docs.browserbase.com/reference/api/create-a-session>
* Browserbase (2026). _Update a session_. Browserbase API reference (vendor documentation). <https://docs.browserbase.com/reference/api/update-a-session>
* Browserbase (2026). _Pricing_. Browserbase. <https://www.browserbase.com/pricing>
* GitHub (2026). _Approving a pull request with required reviews_. GitHub Docs. <https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/approving-a-pull-request-with-required-reviews>
