What Revit Schedules Miss: One Rule for Schedule QA
Sections 5 and 6 contain no measurements. The rule is implementable and the protocol is fixed, but the tool has not been run against a real project model. Everything stated as a result elsewhere in this paper is a definition or a property of the Revit API, not a finding.
1. Abstract
A Revit schedule is a query, and every query has a complement that nobody looks at. Elements that fall outside every schedule in the project are not counted, not priced and not ordered, and nothing in the software reports it: each schedule is complete with respect to its own filters, and Revit holds no notion of a row that should have been there.
This paper proposes one rule for that failure. An element is covered if it appears in the body of at least one schedule, and uncovered otherwise. The rule is binary, it has no classification step, and it carries no exceptions file. Its general form asks whether an element reaches the construction document set at all, by either of the two routes available: a row in a schedule, or a mark in a view placed on a sheet. This implementation computes the schedule route exactly and leaves the view route to section 8.
What the rule cannot see is stated in section 7 and is large: coverage says nothing about whether the values in the row are right, whether an element is double counted across two schedules, or whether the schedule carries the columns the contractor needs. The claim is narrow on purpose. One failure, caught completely, without a dictionary anything can hide in.
2. Introduction
Most of what leaves an architectural office as a number comes off a schedule. Door and hardware counts, window types, room finishes, plumbing fixtures, material takeoffs for an estimate. The drawings carry the intent and the schedules carry the quantities, and the two are produced from the same model by different means.
When something goes wrong with a drawing it is usually visible. A missing family shows as a gap, a broken reference shows as a question mark, a view that fails to regenerate announces itself. A schedule has no equivalent. It lists what its filters select and it is correct with respect to those filters at all times. An element that drifts outside them leaves no trace at all: the schedule does not shrink, because the element was never in it, and the total at the bottom is arithmetic over what remains.
The schedule is not wrong. It is answering a narrower question than the person reading it believes it is answering.
This is the shape of failure the lab keeps finding across different tools, and the reason it is worth a rule rather than a habit: it produces no error, it survives review because the artifact looks complete, and it surfaces at the point where it costs the most, which is procurement or the site.
Section 3 argues that the ordinary way to build this check reintroduces the hiding place it was meant to remove. Section 4 states the rule and, more importantly, the set it ranges over. Sections 5 and 6 are the measurement, not yet run. Section 7 is the refusal: what a coverage report must never be read as saying.
3. The Classification Trap
The natural way to build schedule QA is to classify. Decide what each element is, decide which schedule an element of that kind belongs in, and flag the ones that are somewhere else. This is how model checking is normally done and it is how most commercially available BIM QA rules are written.
It requires a mapping table, and the moment it meets a real model it requires a second file: the exceptions. A great many elements legitimately belong in no schedule at all, and without somewhere to record that, the first run buries its genuine findings under them. So the exceptions file is written, and it acquires three properties, none of which are recoverable later.
It grows in one direction. Entries are added on the day a report is too noisy to read. No one has ever opened an exceptions file to test whether an entry is still deserved, because nothing in the report is evidence about the entries that suppressed themselves.
An exception and an oversight look identical once written. The file records that this category is not reported. It does not record whether that was a considered decision about the project or a way to get through a Friday. Six months later the two are indistinguishable to everyone, including the person who wrote the line.
It travels.The file is written against one project's conventions and then carried into the next project, because rebuilding it is the tedious part and reusing it is free. The conventions do not travel with it.
The check built to find what is hiding therefore ships with the one place in the process where something can hide, and the hiding place is the part nobody audits. The argument here is not that classification is wrong in general. It is that for this particular failure it is unnecessary, and paying for it buys a liability.
4. One Rule, and the Set It Ranges Over
The rule is stated once: an element is covered if it appears in the body of at least one schedule in the document, and uncovered otherwise. There is no second rule and no per-category variant.
Coverage is not recomputed from the schedule definitions. Membership is read back out of Revit itself with FilteredElementCollector(doc, scheduleId), which returns what a given schedule actually contains, with its category, its filters, its phase and its design option already applied. This matters more than it looks: reimplementing schedule filters would mean maintaining a second, drifting copy of Revit's own selection logic, and every disagreement between the copy and the original would surface as a false finding. The union of those collectors across every schedule is the covered set, and the check is a set difference.
The general form of the question is broader than schedules. An element reaches the construction document set by one of two routes, and the rule as implemented watches one of them:
| Route to the document set | This paper | If it fails |
|---|---|---|
| a row in a schedule | computed exactly | not counted, not priced, not ordered |
| a mark in a view placed on a sheet | out of scope, section 8 | nobody building it ever sees it |
The harder half of a binary rule is always its denominator, and this one has the same problem the zoning corpus had. Asking every element in the document whether it is scheduled produces an enormous and useless answer, because a Revit document is mostly not model elements. Four exclusions define the set, and the property that matters is that every one of them is answerable from the API rather than from a curated list:
| Excluded from the denominator | Answered by |
|---|---|
| types, as opposed to instances | WhereElementIsNotElementType() |
| view-specific elements: annotation, detail items, drafting content | OwnerViewId is not InvalidElementId |
| categories Revit offers no schedule for | the API list of schedulable categories |
| schedules that list no model elements: key schedules, sheet lists, view lists, note blocks, revision schedules | ScheduleDefinition, on the schedule side |
That last column is the whole distinction the paper rests on. An exceptions dictionary encodes somebody's opinion about a project and cannot be audited. An API-derived exclusion encodes a property of the software, is identical on every project, and can be checked by anyone with the same model open. The rule stays one rule because the noise is removed by facts rather than by entries.
5. Filter Drift
The mechanism the rule is expected to catch is filter drift. Schedules are built early, when the model is small and the filters are written against what exists that week: these levels, this phase, this workset, this value of a type parameter. The model then grows for a year. Elements added later, on a level added later, in a phase that was not contemplated, or in a design option that never became primary, fall outside filters that were correct when they were written.
How much of this a real project carries is the measurement, and it has not been run. The protocol and the column headings are fixed here, before the first model is opened, so that the result cannot be shaped to the argument afterwards.
| Per model | Value |
|---|---|
| schedules in document | not yet measured |
| elements in denominator, per section 4 | not yet measured |
| covered | not yet measured |
| uncovered | not yet measured |
| uncovered, hand-split: genuine miss | not yet measured |
| uncovered, hand-split: correctly unscheduled | not yet measured |
To run: the tool against real project models, several of them, at different stages. Then the uncovered list from each is split by hand into genuine misses and elements that correctly belong in no schedule. The ratio is this paper's headline number, and the genuine misses are the evidence that the failure is real rather than theorised. A model whose team already knew about every miss found is a null result and gets reported as one.
6. The Noise Problem
The refusal to keep an exceptions dictionary is the paper's argument and also its largest risk, and the two are the same decision. Section 4 removes the noise that the API can identify. Whatever remains after that is a report a person has to read every single run, and if the first run on a real model produces hundreds of rows of things that were never going to be scheduled, nobody runs it a second time. A check that is not run has a precision of zero.
The proposed answer is to fold the report rather than filter it. Uncovered elements are grouped by category, so that a category with four hundred uncovered instances is one line and one decision instead of four hundred. Nothing is suppressed and nothing is written to a file; the folding is a property of the display, and expanding a line shows every element behind it. The distinction from a dictionary is that a folded report still contains everything, so a category that should have been scheduled cannot vanish by being folded.
Whether that is sufficient is an empirical question about how many decisions a first run actually costs, and the threshold has to be named before the run rather than after it.
To decide together, before running: the number of distinct decisions a first run may cost before the approach is called unusable. Set it now, in this file, and let the measurement fall where it falls. If a folded report on a real model exceeds it, this section reports that the no-dictionary position failed on usability, which is a result and gets published as one.
7. What Coverage Does Not Say
A coverage report answers one question and is blind to the rest by construction. Four readings of it are wrong, and the first is the one a reader will reach for.
(1) Covered does not mean correct. An element with the wrong fire rating, the wrong mark, the wrong type or an empty parameter appears in the schedule and is covered. The rule inspects membership and never inspects a value. A model can be 100% covered and produce an entirely wrong door schedule.
(2) Covered once does not mean counted once. An element appearing in two schedules satisfies the rule and may still be double counted in a takeoff. The grid holds that multiplicity and the binary rule discards it. Reporting it would be a second rule, which is a different paper.
(3) The schedules themselves are not checked. Whether a schedule carries the fields the contractor needs, sorts in a usable order, or groups by the right key is untouched. Every element can be covered by a set of schedules that is useless to the person receiving them.
(4) Linked models are out of scope. Structure and MEP usually live in links, and a host schedule does not generally reach into them. The honest scope of a run is one model, and a coverage report on the architectural model says nothing about the consultant models federated with it.
Stated positively: this is a completeness check on the schedule set, not a QA system. Its value is that within its one question there is nowhere to hide, and its cost is that the question is small.
8. Limitations and Next Experiment
The missing half is the view route from section 4, and it is missing because it is genuinely hard rather than because it is uninteresting. It is the more consequential of the two: an element on no sheet is invisible to the people building the project, whatever the schedules say.
Visibility is not a property of an element, it is a relation between an element and a view, and Revit computes it from view range, crop region, visibility and graphics overrides, phase filter, design option, section box, and in three dimensions actual occlusion by other geometry. FilteredElementCollector(doc, viewId) answers most of it, but it has to be asked once per view across a full sheet set rather than once per schedule, and the edge cases carry judgment: an element technically present in a view, an eighth of an inch long behind a wall hatch, is visible to the API and not to a builder. The binary crispness that makes the schedule rule worth stating degrades there, and a rule that needs a threshold is no longer one rule.
The tool is designer-side, not reviewer-side. It runs against the live model, which is why it is exact and cheap, and which also means Revit has to be open with the project loaded. It is something an architect runs before issuing, not something an owner or an authority runs on a file they received. That role needs a format that travels, and in this lab it belongs to the IFC work rather than here.
Sections 5 and 6 remain unmeasured. Until they hold numbers this is a proposal with an implementation plan, not a result, and it is listed on the research index as unpublished for that reason.
A coverage report is not a quantity guarantee and not a code check. It reports that elements appear in schedules, which is neither a statement that the quantities are right nor that the design complies with anything. Nothing here removes the need for review by the professional responsible for the documents.
What has to exist before this is a paper
- The tool: the collector union of section 4, the four denominator exclusions, and a report folded by category.
- Real project models to run it against, with permission to quote counts from them, anonymised.
- The hand split of section 5, done once per model, recorded with its date and who did it.
- The usability threshold of section 6, written down before the first run.
- Verification of the API surfaces named above against the Revit version actually used, since method names and behaviour differ across releases.