DEV Community

Cover image for Tortoise: a slow agent that reads your council's planning applications so you don't have to
Biprayan Choudhuri
Biprayan Choudhuri

Posted on AI-assisted

Tortoise: a slow agent that reads your council's planning applications so you don't have to

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

Most people find out what's being built next door when the scaffolding goes up.

It was never a secret. In the UK, every planning application is published on the council's website, and neighbours get a few weeks to comment. But "published" means a weekly list of twenty-odd entries in planning shorthand, each linking to a pile of PDFs, with the comment deadline tucked away in a separate tab. Nobody reads that website. I wouldn't either.

So I built Tortoise, an agent that reads it for you: slowly, once a week, one page at a time.

What I Built

Every Monday at 07:00 UK time, Tortoise:

  1. Opens the council's weekly list of new planning applications in a real browser.
  2. Keeps only the ones near you. You give it street names and postcode sectors.
  3. Reads their documents: the application form, the design and access statement, the planning statement.
  4. Emails you one card per application: what's proposed, why a neighbour might care, the date you can comment by, and a link to the council's page.

That's the whole product. One email you can read in a minute, instead of a website you would never open.

Who it's for: anyone who'd like a say in what gets built on their street but will never read a planning portal. That includes neighbours, residents' associations, local councillors and the local paper.

Why UK councils, when I live in India? More than 150 of them run the same planning software, Idox Public Access. An agent that understands one portal understands them all. The catch is that those portals block traffic from outside Europe, so Tortoise's browser runs in Frankfurt (more on that below).

How it rewards laziness: you do nothing. Tortoise does the reading, and it's deliberately slow about it: 5 to 10 seconds between pages, 8 seconds between applications, and only last week's list. In a normal week only one step costs money, and this week that step cost about a fifth of a cent.

Demo

Tortoise has no website to click around. It's a Monday-morning job that runs on my machine, so the demo is what it sends you.

Here is a real week: West Suffolk Council's list for 19 to 23 October 2026, with the York Road area of Bury St Edmunds as the watch zone. This is that run's brief and numbers in the current email design. I've removed house numbers; the real email has them.

The weekly email: one card per application, with what is proposed, why a neighbour might care, the comment deadline with a countdown, and a link to the council's page

Look at the York Road card. The council's listing says "single storey rear extension". The application form, the PDF nobody opens, says "rear and side extension". If you live on that side, that's the one sentence you needed, and Tortoise found it because it actually read the form.

On the tree card, the documents don't give a reason for cutting five metres off the crown, so the brief says "not stated" instead of inventing one.

Code

GitHub logo CodeNeuron58 / tortoise

A slow, steady agent that reads a UK council's weekly planning list and the PDFs behind it, then emails you a one-page brief: what's being built near your street, and the date to object by.

The code is MIT-licensed Python. CI runs 110 offline tests on every push, with no network, no browser and no model. The real run and all six failure tests have their evidence in docs/evidence/ (exported Temporal histories, plus CLI output for the duplicate-run test), so every number in this post has a file behind it.

How I Built It

Temporal workflow: WeeklyWatch (one per council, every Monday 07:00 UK time)
  └─ crawl_list ──▶ zone filter ──▶ fetch_application (+8 s pause) per app
        └─▶ gemma_filter (local, free) ──▶ write_brief (paid, counted)
              └─▶ send_email ──▶ record_run (the Lazy Payoff numbers)
Enter fullscreen mode Exit fullscreen mode

1. The browser: Stagehand on Browserbase

The crawl is a Stagehand script running in a Browserbase session pinned to eu-central-1, because UK planning portals refuse traffic from other regions. The PDFs download through the same session, so every request comes from the same place.

Idox portals share one page structure, so the stable parts are read with direct selectors:

  • the weekly-list form,
  • the results table,
  • the Documents tab,
  • the Important Dates tab, where the real comment deadline lives.

Stagehand's plain-English act, extract and observe are the fallback for councils whose layout differs, with instructions like "set the list period to last week" and "click the Next page button". In this week's run the selectors covered everything, so the model that drives Stagehand was never called and the crawl cost nothing but browser minutes.

That driving model is pluggable. I wrote an adapter (crawl/llm_adapter.py) that sends Stagehand's model calls to any OpenAI-compatible endpoint: local Gemma through Ollama, or Ultramarine.

The politeness rules are in code:

  • It never opens the tabs that list applicants' names.
  • At most three text documents per application. Drawings, plans and photos are skipped by name, and scanned PDFs are detected and skipped.
  • Every session is released the moment its work is done. The whole week took 1.71 browser minutes across three sessions.

2. The slow part: Temporal

The whole agent is one Temporal workflow, WeeklyWatch, run per council per week by a schedule (0 7 * * MON, Europe/London). Temporal is what lets a slow agent also be a reliable one.

  • Crawling resumes mid-list. crawl_list heartbeats after every results page, and each page's rows are saved before the heartbeat. If the worker dies, the retry reads the last heartbeat and jumps straight to that page, since Idox page links carry the page number. Rows are upserted by council and reference, so nothing duplicates.
  • The politeness survives crashes. The 8-second pause between applications is a workflow.sleep, so a crash can't skip it.
  • Failures are handled honestly. Activities retry three times with backoff. A bad API key or empty credit is non-retryable, and the email says the brief was skipped and why. If Ollama stays down, every application is kept and the run is marked "unfiltered". Tortoise never silently drops an application.
  • No double emails. Manual runs use the workflow id watch-<council>-<week> with a reject-duplicate policy.

Then I broke it on purpose, six ways, against the live stack. Each test's evidence is in the repo. Here is test 1 in Temporal's timeline: the crawl's first attempt (red) died with the worker after page 2, and the second attempt picked up at page 2 on a new worker.

Temporal timeline: crawl_list attempt 1 killed with the worker after page 2, attempt 2 resumed at page 2 on a new worker, then fetch_application with the 8-second pause

# What I broke What happened
1 Killed the worker after results page 2 Heartbeat timed out after 2 minutes; the new worker logged resuming crawl from page 2. 23 rows, no duplicates.
2 Ended the Browserbase session mid-crawl CDP connection closed, then a retry in a fresh session from page 2
3 The brief failing Three attempts, then an email saying the brief was skipped, and why. The exported run is a real failure: Ultramarine returned an empty reply.
4 Stopped Ollama Gemma failed three times; both applications were kept and the run marked unfiltered
5 Ran the same week twice Workflow execution already started, and one email
6 Stalled the crawl for 150 seconds, like a slow site The first attempt was cancelled at 2:00; the retry crawled all 3 pages and finished

3. The free filter: Gemma 4, on my laptop

First, a plain-Python filter matches street names and postcode sectors. That took this week's list from 23 applications to 2 before a single application page was opened.

Then Gemma 4 E4B runs locally through Ollama's OpenAI-compatible endpoint, at temperature 0 in JSON mode. For each application it returns:

  • a type: extension, new homes, change of use, demolition, trees, mast or antenna, shopfront or advert, or other,
  • likely street impacts, such as parking, noise or daylight,
  • a relevance score from 0 (elsewhere) to 3 (on your street),
  • and a one-line summary.

Tortoise keeps 2 and 3. The JSON parsing is strict and tested against broken replies.

It took 82 seconds for two applications on my laptop. That's slow, and it's fine: it's a Monday-morning job, nobody is waiting, and it's free.

4. The one paid step: Ultramarine 1 Flash

Paper Instruments' Ultramarine 1 Flash is called through the standard OpenAI Python SDK; only the base URL, key and model name change. It receives each kept application's council listing plus the text of its documents, trimmed to a 60,000-token budget (the model accepts 131,072).

The prompt's rules:

  • Use facts from the listing and documents only.
  • Write "not stated" when a fact is missing.
  • Never name applicants, agents or any other private individual.
  • Reply in JSON.

The comment deadline and the link are filled in from the council's own pages, never from the model.

Every call is costed to the token and logged in SQLite, and the total goes in the email's footer:

cost (USD) = ( 0.15 × uncached input
             + 0.03 × cached input
             + 0.50 × output ) / 1,000,000
Enter fullscreen mode Exit fullscreen mode

This week's brief used 4,187 tokens in and 2,852 out, for $0.0021. Every brief in the cost log, nine of them from my first test run to the last failure test, cost $0.012 together, out of the $5 Hacktoberfest credit.

5. The email: Brevo

The email goes out as designed HTML and plain text through Brevo's free plan. Cards are sorted by deadline, each with a countdown, and the run's numbers are in the footer. If the brief was skipped, the email says so instead of going quiet.

What broke on the way

A few things I got wrong before the run above, all fixed now:

  • The in-session PDF download built invalid JavaScript, so early runs read zero PDFs and their briefs had only the council's one-line listing to go on.
  • Comment deadlines came from the summary tab. The Important Dates tab is the one that's right.
  • The brief prompt never actually included the proposal text.
  • Browser sessions stayed open until Browserbase's timeout instead of being released.
  • Cached tokens were billed twice in my cost formula.
  • Ultramarine once returned an empty brief because its reasoning used up a small output cap. The cap is now 8,000 tokens.

The robots.txt lesson

After my live runs, I checked robots.txt properly. Every Idox portal I could reach, West Suffolk and Sevenoaks plus twelve more, says:

User-agent: *
Disallow: /
Enter fullscreen mode Exit fullscreen mode

I should have checked before the first run, not after. A run reads about seven pages in four minutes, with pauses between them. But I ran it many times while testing, and a polite crawler is still a crawler.

So Tortoise ships with both example councils disabled, and a council with enabled = false cannot start a run. The README says to switch one on only with that council's permission. For a project whose whole point is helping people take part in local decisions, asking first is the only version that makes sense.

Why Does Open Innovation Matter?

  • The boring 90% costs nothing. The crawl logic, the zone filter, Gemma and the scheduler are all open source or open-weight and run on my laptop; only the browser itself is rented, by the minute. If every step went through a hosted model, I'd be paying to read every page of every council's list, every week. Here the only metered step is one short brief about the few applications that matter, which is what makes "every council, every week" thinkable for a residents' group with no budget.
  • I could fix the framework instead of working around it. Stagehand is open source, so I could read exactly how it calls its model and write an adapter that sends those calls to a local model instead. A closed browser agent would have made that choice for me.
  • Most of the data stays at home. Filtering the whole list, by street and then by Gemma, happens on my machine. Only the few kept applications' documents go to a hosted model, and the brief is forbidden to name anyone.
  • The rules are inspectable. Politeness and privacy are code, not a promise on a terms page: the pauses, the three-document cap, never opening the contacts tab. A council's IT team can read exactly what Tortoise will and won't do before saying yes.
  • Nothing is locked in. Gemma, Stagehand's driving model and the brief writer all sit behind OpenAI-compatible endpoints, switched with environment variables. Temporal runs locally with a database file.

The Lazy Payoff

The email's footer: 23 applications listed, 2 in your zone, 2 PDFs read for you, 7 pages read, 1.71 browser minutes, $0.0021 total cost

The real week: West Suffolk, 19 to 23 October 2026 Result
Applications on the weekly list 23, over 3 results pages
On the watched streets 2 (York Road and Springfield Road)
Documents read for you 2 application forms, 13 pages; drawings skipped
Comment deadlines found 10 and 14 November 2026
Browser time 1.71 minutes, 3 sessions, all released
Gemma, running locally 82 seconds, $0
Ultramarine 7,039 tokens, $0.0021
The whole run about 4 minutes, from start to email
Time a human spent reading about a minute, on the email

Doing this by hand means:

  • opening the weekly list,
  • checking 23 addresses against your streets,
  • opening two applications and finding each deadline in its own tab,
  • and reading 13 pages of forms.

Call it half an hour a week. Mostly, though, people don't do it at all, and that's the real cost: the deadline passes without them.

To be honest about usage: so far, the only inbox Tortoise has emailed is mine.

Prize Categories

  • Best Use of Browserbase: Stagehand drives each council's live planning portal in a Browserbase session pinned to eu-central-1 (UK portals block other regions). It downloads the PDFs through that same session and releases it the moment it's done, so a full week took 1.71 browser minutes. Temporal turns that agent run into a repeatable Monday workflow.
  • Best Use of Ultramarine: Ultramarine 1 Flash, called through the OpenAI SDK, reads the kept applications' documents and writes the brief under strict "not stated, never guess, never name anyone" rules. Every call's tokens and dollars are logged: this week's run cost $0.0021, and every brief in the cost log, tests included, cost $0.012.
  • Best Use of Temporal: the whole agent is one durable WeeklyWatch workflow on a Monday schedule. It has per-page heartbeats so a crash resumes mid-crawl, a durable 8-second politeness sleep, non-retryable auth errors and duplicate-run rejection, proven by six live failure tests with their evidence in the repo.
  • Best Use of Gemma: Gemma 4 E4B runs locally through Ollama and labels every in-zone application (type, street impact, relevance 0 to 3) for free, so only the applications that matter reach a paid model. If Ollama is down, nothing is silently dropped.

Thanks for reading. If you live in one of those 150-odd council areas, I'd love to know what you'd want the Monday email to tell you that it doesn't yet. And if you work at a council, I'd love your permission to point a very polite tortoise at your weekly list. 🐢

Top comments (0)