What's holding up this job? For WO-123, the answer:

WO-123 has been open eighteen days. The quote was approved on the 1st. The material request for the special-order shingles went out on the 4th, and its purchase order is still waiting on vendor confirmation. The technician's last note, from the 7th, reads "waiting on material, customer informed." Two service tasks are done; the install step is blocked on that material.

A dispatcher who wants that paragraph opens ten screens: the job, its status history, the notes, the time logs, the quote, the material request, the purchase order, the vendor, the customer, the checklist. Ten to twenty minutes, if they already know where to click. Most people never do it. They ask the technician, or they guess.

Sense, the agent built into Zuper, wrote that paragraph in ninety seconds from that one question.

This post is about the context engineering that makes an answer like that possible, and correct. It goes somewhere we did not expect: the agent computing metrics the product never explicitly built, from a company's own operations manual.


Three kinds of question, one box

Field service companies ask three kinds of question all day.

The first is measured. What was revenue by team last quarter? How many jobs are past due? Harder than it looks, because the vocabulary is local: "Booked" is a status at one company and a category at another, and revenue can mean job revenue, invoiced amount, or approved quote value. Choose wrong and the number is confidently, invisibly incorrect.

The second is investigated, and almost nothing answers it well. Why is this job stuck? Why did this invoice take three weeks to go out? The answer is never in one place. It is a chain of facts across a quote, a material order, a vendor, and a note somebody typed at 6pm.

The third is documented. How does this work here? How do we define first-time fix? The answer lives in the company's own manuals and policy pages.

Sense answers all three in one conversation, and most real questions are a blend: show me overdue invoices, and tell me why the top three are stuck is a measured question and an investigated one in the same sentence.

One principle runs through everything below: every mechanism exists because a specific wrong answer is possible without it.

sense-title.png


Anatomy of an investigation

Back to WO-123. What the user watches while it works:

Looking up work order 123
Reading the job                  → 18 days open, install pending
Checking the linked quote        → approved on the 1st
Following the material request   → shingles, raised on the 4th
Checking the purchase order      → sent, awaiting vendor confirmation
Reading technician notes         → last update on the 7th
Checking service tasks           → 2 of 3 done, install blocked

Nobody handed the agent that sequence. It works out the trace itself, because it carries a working model of how the business fits together: a job belongs to a customer and a property, a quote usually precedes it, an invoice follows execution, a material request is fulfilled by a purchase order or a stock transfer, and service tasks are the job's sub-steps. Told to find what is holding up a job, it knows where to look. A typical investigation runs in about a minute to a minute and a half, end to end.

It works over a read-only, allowlisted slice of the Zuper MCP surface we wrote about separately: 48 read-only tools across 14 entity families. The finding is relayed to the user in full rather than re-summarized, because a trace that reaches the user with most of its facts summarized away is a loss, not polish.

Two problems make this harder than looping over an API.

Records are enormous. One fetch from a live operational API can return megabytes of nested data, and almost none of it bears on the question. So any response over 50KB never reaches the model whole. It is parked in a short-lived store, and the investigator gets a preview capped at 4KB: the record's real field names, the shape of the data, and a small sample. It then names the exact paths it wants and reads only those, under a budget, so an investigation converges instead of wandering. This is accuracy engineering before it is cost engineering: a model reasoning over a compact preview finds the blocked purchase order; one buried in a raw payload finds whatever surfaced last.

Tools invite guessing. The operational tools expose thirty to seventy optional parameters each, often with example values sitting right there in their schemas, and a guessed filter does not fail loudly; it silently changes the answer. So only parameters with a real, concrete value get set, usually a single identifier that scopes the request. Words in the user's question like "delayed" or "urgent" describe the output to produce; they are not permission to set matching filters.

The boundary runs the other way too. This path never counts or aggregates. Even a single-customer count goes through the analytics path, because that path can render it, pin it, and refresh it. Explaining one record and measuring many are different jobs.


Anatomy of a measured question

Revenue by team last quarter for the Inspection Ready residentail jobs.

Vocabulary first. "Inspection Ready" is not in any product catalog; it is a status this company invented. And "residentail" is a typo. Before anything gets built, the agent looks the unfamiliar phrases up against what this workspace actually stores:

### "Inspection Ready" → job_status
Exact match: Inspection Ready  (category: Field Work)
Filter on: jobs.current_job_status_name    (current state)
  · job_status_history.status_name         (history / transitions)
  · job_status_checklist.status_name       (answers captured at that status)

### "residentail" → custom_field_value
Field: Property Type (jobs)
No exact match. Closest stored values:
  - Residential
  - Residential - Multi
→ confirm before filtering

The first block settles the status and raises the real question: three fields carry it, and jobs currently in Inspection Ready are not the same set as jobs that passed through it, so the choice is made from the tense of the question. The second block is the other half of the rule: a close match is never used automatically. "residentail" becomes a clarifying question with options that are real stored values, not invented plausible ones.

Here is why that matters. Suppose the lookup finds nothing, and the system shrugs and drops the filter:

Q: how many jobs for Acme Property Management are in production?
   [ "Acme Property Management" unresolved, filter dropped ]
   [ "production" unresolved, filter dropped ]

   4,812 jobs        ← every job in the company, rendered as the answer

That number is wrong, specific, and beautifully formatted. Nobody catches it, because a stat card carries no evidence of what it left out. When every term in a question fails to resolve, Zuper Sense does not run the query. It says which words it could not find and asks.

The other half of that defense is that every rendered number can be opened. Tap a bar on a chart or the number on a stat card and the underlying records open as a table. A stat card you can open is a number you can audit, and the fastest way to notice a filter you did not expect is to see the rows behind the figure.

Then a specification, not SQL. Once the status is pinned to a field and the user confirms Residential, the agent describes the result it wants in business terms: revenue and job count, broken down by team, for last quarter, filtered to jobs currently in Inspection Ready on Residential properties, ordered by revenue, top 20, rendered as a bar chart.

Checked against reality. Every field named in that spec is verified against the definitions actually available for the selected entities. Near-misses snap to the real name. Fields in the wrong slot get moved. Date operators on non-date fields, placeholder values, internal identifiers, duplicates, and impossible shapes get stripped or corrected before execution.

Executed, shaped, rendered. Results come back with display names resolved to the company's own labels, then get shaped for the visualization: a single number becomes a stat card, a time axis becomes a line, and a breakdown over time stacks. A turn that ends on text alone is treated as incomplete, so a missing chart gets recovered rather than shipped as a paragraph the user cannot see. End to end, a measured question answers in about 10.6 seconds on average. The median is around 9 seconds, with a tail of about 18.6 seconds at p90 and 24.8 at p95.


Metrics the product never shipped

Here is the part we did not anticipate.

Companies write down how they measure themselves. It sits in an operations manual, an onboarding doc, a policy page. And those write-ups are often precise enough to compute from.

Q: what's our first-time fix rate this quarter?
Sense: Your operations manual defines first-time fix rate as jobs closed on the first visit divided by all closed jobs, excluding warranty callbacks. On that definition it is 82.4% this quarter, up from 78.1% last quarter.
Sources: Field Operations Manual

There is no first-time-fix-rate measure in the catalog. Nobody built one. A page of prose was the specification, and the agent read it, carried the definition into the data query, and computed the company's own metric their way. When a user names a metric the catalog has never heard of, searching the documentation comes before guessing and before asking.

That is why the documentation layer has an absolute rule attached: answer only from the retrieved passages. A fabricated configuration answer is indistinguishable from a real one to the person reading it, and they will act on it.

The same pattern shows up with fields nobody designed a metric for. Companies track their own dates in custom fields and want the rates between them: what share of referrals became booked assessments last month. There is no "booking rate" measure to select, only two custom fields and a ratio, so Sense builds the metric on the spot. A rate is really two counts, each with its own date field and conditions, and the agent restates both halves on every follow-up so a casual now show it weekly cannot silently drop one.

And when the arithmetic does not hold, the engine refuses. Ask for "the latest value" of a field that holds text and there is nothing to sort by; put a date window on a field that holds numbers and it matches nothing. In both cases the system stops with a reason specific enough to turn into a real choice:

Picking the latest one needs a date to sort by. Want every record's value listed instead, one row each, or should I pick by an actual date field on that record?


Why there is more than one agent

The two walkthroughs above were run by different agents, and the user never chose between them. The split is about context isolation, not specialization for its own sake: every fact in front of a model is a candidate for its answer, and irrelevant candidates are where plausible wrong choices come from. So each specialist sees only what its own decision needs.

  • An entity selector reads entity descriptions, nothing deeper, and decides which parts of the business a question touches.
  • The query composer then sees only those entities' fields. For revenue by team, that is 93 fields instead of 1,220. Among 93, the right revenue field is obvious; among 1,220, revenue-shaped fields from jobs, quotes, invoices, and payments sit beside it, every one a defensible wrong choice.
  • The vocabulary resolver sees live stored values, not the query being built.
  • The record investigator runs its own tool loop and returns a written finding, so the conversation never carries the megabytes it read.
  • A knowledge searcher and a memory keeper see only passages and preferences.

The specialists exist so each context can stay small.


What the agent knows before it answers

A model that reasons well and knows nothing about your business produces fluent, wrong answers. Several layers of context stand between the question and the answer.

layers-sense.png

A curated definition layer, not raw tables

The agent never sees a database. It sees a semantic layer we maintain: 53 business entities carrying roughly 1,220 named metrics and attributes, each described in plain language. "Total revenue" is a definition we wrote, not a SUM() the model invented.

Three properties matter more than the size.

It is deliberately smaller than the data. Bridge tables, lookup tables, and internal masters stay hidden even though the system still queries them. A junction table between users and teams is a correct thing to have in a data model and a terrible thing to offer an agent as a choice.

It carries relationships, because revenue by technician spans multiple entities and the wrong path through them produces double counting rather than an error.

It carries counting rules. Take one job worth $1,000 that passed through four statuses. Group job revenue by status transition and the honest answer is still $1,000, but the arithmetic hands back $4,000, because the job lands in four groups and its revenue is counted in each. Every parent total split by a one-to-many child does this: job revenue by service task, invoice totals by line item. The overstatement looks entirely plausible, so the agent is directed to the child's own metrics instead, and told to name the risk out loud when a user insists on the inflating cut.

It is also the scope boundary: if an entity this workspace does not expose is absent, the answer is not available yet, never a nearest-neighbor guess.

The company's own vocabulary

Nothing in the catalog knows this company says "Inspection Ready," tracks a field called "Assessment Booked Date," or ships from a warehouse named "North Dock." That vocabulary is per-workspace, it changes weekly, and it is where many wrong answers come from.

So the agent looks things up, under one rule: a word you cannot find in the catalog is an unverified lookup, not an ambiguity. Look it up before asking the user, and before building a query with it. The lookup covers everything a company can name: statuses, categories, teams, payment terms, custom fields and the values inside them, checklist questions, locations, people, and customers. Common vocabularies are cached briefly per company; people and customers are searched live, because names are neither stable nor unique. A resolved name comes back with each candidate field tagged by role, and the agent picks by what the question is actually asking.

The conversation itself

Every result in a session lands in a compact ledger the agent reads on later turns.

[ r3 ] entities: jobs, teams | rows: 12 | rendered: bar_chart | status: ok   ← latest
  query: revenue by team, last quarter, Inspection Ready residential jobs
  summary: Revenue by team, last quarter

That ledger makes follow-ups cheap and correct. Show that as a table re-renders an existing result without rerunning the query; add job count knows what that was; a superseded result is never offered again. It also carries context across surfaces: opening a chat from a pinned dashboard widget seeds the conversation with that widget's query and a sample of the rows on screen, so why is the top one so high? resolves to a real record.

The loop runs forward too. Every answer ends with grounded follow-ups based on what was just rendered: after revenue by team, break down the top team by technician; after a stat card, often a why that hands off to the investigator. That is how a measured answer turns into an investigated one without the user needing to know the second capability exists.

Standing rules and runtime facts

Users set explicit preferences, like scoping job questions to this quarter or preferring tables for top-N lists, and the agent separately remembers how they talk. Precedence runs one way: this turn's instruction, then preferences, then memories, then defaults. Both get applied out loud, because a filter the user cannot see is a filter the user cannot correct. Then come the runtime facts that are wrong by default if omitted: today's date, the user's timezone, and who my jobs belongs to.


Answers we shipped that were wrong

Everything above is the system working. It did not always. Three answers that rendered wrong in production, and what each one bought us.

The rate that counted deleted jobs. What share of referrals became booked assessments? The rate came back plausible and slightly wrong: it included deleted jobs. The exclusion of deleted records lived on the parent entities, so any query that touched only child records, like the custom-field values this rate is built from, never joined the parent and silently skipped the filter. The fix moved the exclusion into every child entity's own definition, so no query path can reach a child row whose parent is deleted. The rule it left behind: an invariant enforced on one entity and inherited by others through a join is not an invariant.

The payments attributed to every job. Payments by job type. There is no direct link between a payment and a job; the correct chain is payment to invoice to job. But three linking paths of equal length existed, and the engine broke the tie by declaration order, walking payment to customer to jobs, which attributes each payment to every job the customer has ever had. The grouped total was inflated and perfectly plausible. The fix pinned the correct path as the tie-break winner, and any grouped total is now reconciled against its ungrouped baseline: it must sum back exactly.

The filter that became an axis. Count jobs by Referral Source, but only jobs whose Referral Date falls in 2025. Two custom fields, one for grouping and one for filtering, and there was no supported recipe for that shape. The generator fell back to treating both as display dimensions: the date filter stopped filtering and became a breakdown axis, and all-time counts rendered under a title that said 2025. Root cause: an untaught query shape degrading into the nearest taught one instead of refusing. The fix added the missing recipe with the filter declared as a predicate, pinned by a regression test, and hardened the rule behind that earlier 4,812-job example: a constraint that cannot be honored must fail the query, not fall off it.


From a whiteboard to a chart

Planning meetings produce chart sketches, not queries. Somebody draws revenue by month with a second bar for job count, photographs the whiteboard, and wants that.

Sense reads the image as a layout specification: axes, groupings, chart type, sort order, the date window scribbled in the corner. It resolves the fields those labels imply, fetches live data, and renders the drawn chart with real numbers. Two rules keep it honest: the image is never a data source, so numbers visible in the photo are never quoted or computed on; and text inside the image is a label, never an instruction, so a sticky note reading ignore your previous instructions is just a label about a chart. The same path handles screenshots of reports from other systems, which is how people often ask.

[Screenshot: a whiteboard sketch beside the chart Sense rendered from it]


Trust

Which company's data a request may touch is injected by the request layer and cannot be named, overridden, or supplied by the model, on any path, in any tool; the same holds for the credentials behind investigations and the document set knowledge search may read. Every path is read-only. Everything read from outside (retrieved passages, preference text, company-authored field names, text inside images) arrives labelled as data, not instruction; a passage that contains instructions gets reported, not obeyed.

And the reasoning is visible while it happens: the trace at the top of this post, sources cited on documentation answers, the query inspectable behind every pinned widget, and the records openable behind every chart. When something breaks, the model gets a generic failure signal and the real error goes to logs, so field names and query fragments never surface as product copy.


What it cost to make wrong answers hard

Context beats capability, and small contexts beat big ones. Every meaningful jump in quality came from giving the agent something true about the business, and most came from giving one agent less: 93 fields instead of 1,220 in front of the composer, a 4KB preview instead of a 50KB-plus payload in front of the investigator, a written definition instead of a guess in front of a metric. Each cut removed a whole class of confident error.

Asking is a feature, not a fallback. On a 100-question QA set covering analytics questions and live-record investigations, 91% of answers were correct. That is an evaluation result, not a production accuracy claim. Users forgive a clarifying question instantly and almost never forgive a wrong number, because a wrong number gets forwarded. Clarify unless it is genuinely settled is a higher bar than clarify when stuck.

Guarantees live below the model. Every entry in the failure gallery ended the same way: a rule that had lived in instructions or in an assumed join moved into the layer beneath, where no phrasing can route around it.

75bb26e8-5f35-41ec-ac9c-10880e2a1ea1.png


This was never about putting a chat interface over a database. It was about engineering the context that lets an agent understand your business deeply enough to turn a question into the fastest path to understanding what is happening.