Inspiration

A film's shooting location is a financial decision worth tens of millions of dollars, and it is currently made on PDFs of uncertain vintage, a spreadsheet somebody's assistant maintains, and a phone call to a line producer who has worked there before.

The reason it stays manual is that the honest version of the question is genuinely hard. A headline "30% transferable credit" is not a 30% incentive. Sold to a broker at 88 cents, landing eighteen months after wrap, against a crew base thin enough that four department heads fly in and house for twenty weeks, it is somewhere near 21% — and that gap is the entire decision. Nobody publishes the 21%.

So the obvious build is an LLM that answers "what is Georgia worth?" And the obvious build is unusable, for a reason that has nothing to do with model quality: a confident wrong number and a confident right number look identical, and the person reading it cannot tell which they have. A budget is not a chat transcript. You cannot ship a number into a financing conversation that you cannot trace back to a statute.

We wanted to find out what it takes to make a research agent whose output a producer could actually sign off on — and the answer turned out to be architectural, not prompt engineering.

What it does

Backlot takes a screenplay and a list of territories, and returns what each one costs to shoot in — with every figure traceable to the verbatim quote it came from.

Five stages, and the whole chain runs in one call:

  • Script breakdown. A Fountain screenplay in; a stripboard out — scene headings, interior/exterior, speaking cast, page eighths. This part is offline, free, and needs no credentials at all. A Gemini pass then reads the action prose for what a parser structurally cannot see: water, fire, stunts, minors, crowds, period dressing. Every requirement comes back carrying the line of prose it was read from.
  • Territory survey. Three researchers run concurrently against one jurisdiction — shoot incentives (14 fields), post and VFX incentives (15), and physical feasibility (16: soundstages, crew depth, historical weather, visas, currency exposure). Each fans out over five or six facets with their own search objectives, because a domain's answers do not live in the same document: a statute states the rate, what is left of this year's allocation is a press release, what passed in March is a bill tracker.
  • Assessment. Verified claims become typed parameters, then a cost function turns them into money — qualified spend, minimum-spend gates, uplifts this production can actually earn, per-project caps, allocation, broker realization, the time value of waiting eighteen months for the cash. It is a pure function. No model, no network, no clock.
  • The summary. One Gemini call writes the assessment up in prose — and every figure in that prose is checked back against the estimate it was given.
  • Watch. An assessment is a photograph. This layer knows when it has gone out of date.

How we built it

Google ADK 2.x for orchestration, Gemini 3.8 Flash via Vertex AI (ADC, no API keys), Parallel for web search and monitoring, deployed to Cloud Run.

The architecture is one chain with layers that only depend downward:

screenplay ── parse ── enrich ────────┐
                                      ├── plan ── cost ── summarise ── CostEstimate
territory ─── survey ── resolve ──────┘                                     │
                                                            watch ── derive ┤
                                                                    reprice ▼
                                                                          Alerts

Four things about that shape are deliberate:

  • Free work happens before paid work. Parsing is offline, so a malformed screenplay fails before sixteen searches have been bought.
  • The two slow branches run concurrently. Enrichment reads the script, research reads the web. They share no inputs. Running them in sequence — as a CLI naturally does — doubles the wall clock for nothing.
  • Uncertainty ends before arithmetic begins. Everything uncertain has happened by the time control reaches the cost function, which is why it can be a plain function a reader re-derives on paper instead of another agent.
  • The last stage cannot change anything. The summariser reads the finished estimate and writes about it; failing prose is discarded whole, never corrected.

The verification rule is enforced by types, not convention. A verified claim cannot be constructed without an excerpt index that only the verifier can supply. A resolved term cannot be constructed without the claim it was read from. A verified change cannot be constructed without a verified excerpt. So no number in an estimate and no repricing in an alert can exist without a quote from a page that was actually retrieved. Where a number had to come from us instead — a broker discount, a day rate, a cost of capital — it is an assumption, quarantined in its own package. A reader can always ask which numbers came from the world and which came from us.

No library imports ADK, Gemini or the network at module level. Every outside call is an injected seam — the search function, the model call, the monitor client, and the progress callback. That is what lets 626 tests run offline in ten seconds against the same code the deployment runs, and it is the only reason the failure paths are testable at all: a facet that raises, a model that fabricates a citation, a retry that comes back worse.

Challenges we ran into

The web is not a closed corpus. The breakdown verifier had it easy — a screenplay is a fixed document, so a model can only misquote it. Against the web a model can cite a page that does not exist, and the citation will look perfect. That forced a fourth check with no equivalent upstream: was this URL actually fetched during this run? We keep a corpus of what each run retrieved and reject the citation before the quote is even considered.

Resolution wanted to do arithmetic, and could not be allowed to. A base rate comes back as "20% of qualified in-state spend, 30% with the promotional logo," because that is what the statute says. Something has to turn that into a number. The temptation is to let the model compute — and a model that invents a rate has produced a budget, which no amount of downstream care recovers from. So the model classifies and never computes: which structure this is, which spend buckets a rate bites on. Every number it returns is checked back against the claim it supposedly read it from, and one that is not stated there is dropped and recorded as an assumption instead.

Gates cannot be averaged. The early design had a composite 0–100 territory score. It was wrong, and the way it was wrong was instructive: a twelve-week permit that makes your start date impossible is not a small number, it is a different answer. Any weighted score launders that into a mild penalty. So gates are never averaged anywhere — not into a score, and not out of the summary either, which is why an omitted gate is a rejection reason. A summary that quietly drops a blocking permit reads as a clean answer, and a clean answer is exactly what gets acted on.

Ordering the cost pipeline. Caps apply to face value, not to cash. A state that caps a credit at ten million caps the credit; the broker's discount comes off afterwards. Applying them the other way round overstates the cap by twelve percent — and nothing about the output looks wrong.

Accomplishments that we're proud of

  • A number you can audit end to end. Every figure in an estimate has a verbatim quote and a URL that was genuinely retrieved behind it. The one place a number came from us instead, it says so and names the reason.
  • The repricing loop. Monitoring that tells you a rank changed, in dollars, rather than that a page changed. It is the same cost function run twice, which means the alert is as trustworthy as the original assessment — and no more, which is also honest.
  • Watching costs are derived, not configured. Nobody is asked to fill in a settings screen. The estimate already ranked what its uncertainty is worth, so exposure picks the cadence: a field carrying real money gets a six-hour check, the same field on a territory nobody is seriously considering gets seven days. And it never watches anything slower than the world moves, because a cadence that misses the change is worse value than not watching at all.
  • 626 tests, offline, in ten seconds. Including the failure paths — fabricated citations, raising facets, retries that come back worse.
  • It is usable with nothing configured. Parsing and budgeting are free and offline. The toggles tell you exactly which stage needs a model, which needs a search key, and which needs real quota.
  • The rejection rate is a first-class output. Diagnostics are printed even on a clean run — a driver list is only meaningful once you have seen it be short.

What we learned

Verification is an architecture, not a step. We started thinking of it as a filter at the end. It became the shape of the whole system: five verifiers, each checking a different claim against a different source, and each carrying its guarantee forward in a type so the next layer physically cannot construct an unverified object. The rule survived because it was structural. Every place we tried to enforce it by convention, it eventually leaked.

The interesting output is not the number. Watching the live pipeline view, the thing that holds attention is a facet that retried, a researcher whose sources were thin, six claims thrown out because their quotes were nowhere in the pages retrieved. That is what tells you whether to trust the total. So the UI shows the run, not just the result.

"Not found" is an answer, and conflating it with failure is a real bug. Most US states genuinely have no cultural test. How much of an allocation remains is usually published nowhere citable. A failed facet is a completely different thing — those fields are unresearched rather than absent — and a system that renders both as a blank has lied to you twice.

Say what you cannot do. We deliberately do not return a live FX rate: web search cannot source one responsibly, and a stale rate read as current is a budgeting error. The field asks for peg status and volatility instead. Naming the limit is more useful than filling it.

Splitting the model's job makes it better, not just safer. Sixteen facets with their own objectives beat one prompt asking for everything, and not marginally — one prompt returns something shallow for every field at once.

What's next for Backlot

  • A real weather API. Historical weather is a data question we are currently answering with a document search, and it returns climate-summary pages of wildly varying quality. This is the clearest upgrade in the repo and we know exactly where it goes.
  • Webhooks for the monitors. Polling gets the entire feature with no inbound surface, which is why we started there. When sub-hour latency is worth an endpoint, replay handling and auth, the poller stays and the handler feeds the same verification path.
  • Comparative shortlists as the primary view. The cost function already ranks and the watch layer already fires on rerank. The web app should lead with six territories side by side and what would have to change to reorder them.
  • Broker discount as a researched term rather than an assumption. It is currently one of the largest numbers that comes from us instead of the world. Transfer market rates are quoted, if not always citably.
  • Schedule-aware feasibility. The research window already changes the feasibility answers. Feeding the actual shoot days from the breakdown into that window, instead of a hand-entered quarter, closes a loop that is already half built.
  • More jurisdictions per run. The concurrency budget is survey-wide and tuned for one territory at a time; a shortlist of six is currently six sequential surveys.

Built With

Share this project:

Updates

Submission history