|
|
| Menu location |
|---|
| Manage → Report Tools → BIM Report |
| Workbenches |
| BIM |
| Default shortcut |
| None |
| Introduced in version |
| 1.2 |
| See also |
| None |
The BIM Report is a tool for generating schedules and data reports from a FreeCAD model. It uses an SQL-like query language to select, filter, and aggregate data, offering a declarative alternative to other scheduling tools. Ultimately, it enables users to create and automatically populate a spreadsheet with data gathered from the model.
You might choose the BIM Report tool when you need to perform:
The BIM Report system consists of three main components: the Report object in the Tree View, the Target Spreadsheet for the results, and the Task Panel where you configure the report.
When you create a new report, you will see two new objects in the Tree View: the Report object, which stores all your settings, and its associated ReportResult spreadsheet.
Double-clicking the DataReport object opens the Task Panel, which is the primary workspace for editing your report. The panel is divided into two main areas: the Report Statements and the Statement Editor.

This section is always visible at the top of the Task Panel and gives you a summary of all the queries (or "statements") that make up your report.
The main feature is a table listing each statement. From here, you can see and directly edit the Description, and toggle key options for each query:
Label, Area) as a header row for that statement's results.Double-clicking on a statement row will open the statement for editing. Pressing F2 on a statement row enables in-place editing of the statement description without loading the full editor.
Below the table are buttons to Add,
Remove,
Duplicate, or
Edit the selected statement.
Finally, the Report Templates dropdown menu allows you to load complete, pre-configured reports, such as a "Room and Area Schedule".
This section appears at the bottom of the Task Panel when you select a statement and click Edit Selected. This is where you write and configure an individual SQL query.

Label, Area) in the spreadsheet output for this statement.The Save button saves the modifications to the loaded statement. When the
Save and Next checkbox is ticked and the
Save button is pressed, in addition to the save operation a new statement will be created for a more streamlined edition experience. The ❌ Discard button closes the statement editor without saving any changes made.
This small tutorial guides you through creating a complete room schedule, from a simple list to a multi-part report with calculated totals.
First, let's create a simple list of all spaces and their areas.
Room List.Spaces and sorts them alphabetically.SELECT Label, Area FROM document WHERE IfcType = 'Space' ORDER BY Label ASC
Now, let's add a second statement to calculate the total area.
Total Area.SELECT 'Total Usable Area', SUM(Area) FROM document WHERE IfcType = 'Space'
Pipelining allows you to chain queries together, where the output of one becomes the input for the next. This is essential for complex hierarchical selections, such as when using the CHILDREN() function. Since it is not recursive, it only finds direct descendants.
For example, to find all doors located on the "Ground Floor," you must perform a multi-step query, because doors are children of walls, which are in turn children of the floor.
Find Ground Floor.SELECT * FROM document WHERE Label = 'Ground Floor'
Find Walls on Floor.SELECT * FROM CHILDREN(SELECT * FROM document) WHERE IfcType = 'Wall'
Find Doors in Walls.SELECT Label, Width, Height FROM CHILDREN(SELECT * FROM document) WHERE IfcType = 'Door'
Understanding the Result: The final spreadsheet will only show the results of the last statement in the pipeline (the list of doors). The first two statements act as intermediate filters whose results are passed in memory but not printed to the spreadsheet, giving you a clean and precise final report.
This section provides a detailed reference for the SQL dialect used by the BIM Report tool.
Note: The most up-to-date reference for all clauses and functions can always be found in the UI by clicking the Help button within the report's Task Panel editor.
SELECT: Specifies the columns of data to return. Can use * to select a default identifier for all objects.FROM: Specifies the data source. In a standalone query, this is always document. In a pipeline, document is a placeholder for the data from the previous step. Can also use the CHILDREN() function.WHERE: Filters objects based on conditions.GROUP BY: Groups rows that have the same values into summary rows, typically used with aggregate functions.ORDER BY: Sorts the final results. Can be followed by ASC (ascending, default) or DESC (descending). Supports sorting by multiple columns.AS: Creates an alias (a custom name) for a column in the SELECT clause, e.g., SELECT Area AS "My Area".The dialect has specific rules for how to use expressions (like functions or arithmetic) in the GROUP BY and ORDER BY clauses.
GROUP BY: When grouping by an expression, you must repeat the entire expression in the GROUP BY clause. Using an alias from the SELECT list is not supported.
SELECT TYPE(*) … GROUP BY TYPE(*)
SELECT TYPE(*) AS BimType … GROUP BY BimType
ORDER BY: Conversely, sorting by a raw expression is not supported. To sort by the result of an expression, you must first include it in the SELECT list with an AS alias, and then refer to that alias in the ORDER BY clause.
SELECT Label, LOWER(Label) AS sort_key … ORDER BY sort_key
SELECT Label … ORDER BY LOWER(Label)
The dialect includes a library of built-in functions for data manipulation and analysis.
| Category | Function | Description |
|---|---|---|
| Aggregate | COUNT(property|*)
|
Counts the number of objects. COUNT(*) counts all rows; COUNT(property) counts rows where the specified property is not NULL.
|
SUM(property)
|
Calculates the sum of a numerical property. | |
MIN(property)
|
Finds the minimum value of a numerical property. | |
MAX(property)
|
Finds the maximum value of a numerical property. | |
| Hierarchical | PARENT(*)
|
Returns the architecturally significant parent of an object (e.g., the Wall a Window is in), transparently skipping over generic groups. |
CHILDREN(subquery)
|
A function used in the FROM clause. Returns all children of objects found by the subquery, resolving both containment (.Group) and hosting (.Hosts) relationships. This function is not recursive. To find grandchildren or deeper descendants, chain multiple CHILDREN() steps together using a pipeline.
| |
CHILDREN_RECURSIVE(subquery, max_depth=15)
|
A function used in the FROM clause. Returns all descendant objects of the subquery results by recursively traversing the hierarchy. It transparently skips generic groups. An optional max_depth can be specified (default: 15, 0 for unlimited). This replaces the need for complex pipelines to find nested objects.
| |
| Utility | TYPE(*)
|
Returns the object's user-facing type, as determined by its proxy (e.g., Wall, Space, Part::Box).
|
CONVERT(quantity, 'unit')
|
Converts a FreeCAD Quantity object to a new unit and returns it as a raw number. Example: CONVERT(Area, 'm^2').
| |
| String | LOWER(property)
|
Converts a text property to lowercase. |
UPPER(property)
|
Converts a text property to uppercase. | |
CONCAT(val1, val2, …)
|
Joins multiple text values and properties together into a single string. |
You can access any property of an object, including nested data structures.
Label, IfcTypeShape.Volume, Placement.Base.zPARENT().
PARENT(*).Label, PARENT(*).PARENT(*).Height| Type | Operators | Description |
|---|---|---|
| Comparison | =, !=, >, <, >=, <=
|
Standard comparison operators. |
LIKE
|
Case-insensitive pattern matching. Use % for multiple characters and _ for a single character.
| |
IS NULL, IS NOT NULL
|
Checks if a property has a value. | |
IN (value1, value2, …)
|
Checks if a property's value is in a given list of literals. | |
| Logical | AND, OR
|
Used in the WHERE clause to combine multiple conditions.
|
| Arithmetic | +, -, *, /
|
Perform calculations on numerical properties. Obeys standard operator precedence; use () to override.
|
') and double quotes (") can be used for text literals (strings). For example, WHERE Label = 'Wall' and WHERE Label = "Wall" are equivalent.--, and multi-line comments, which are enclosed in /* … */.This section provides a collection of practical queries that you can use directly or modify for your reports.
Select all structural elements (Columns and Beams):
SELECT Label, IfcType
FROM document
WHERE IfcType IN ('Column', 'Beam')
ORDER BY IfcType, Label
Select all walls with "Exterior" in their name:
SELECT Label, Length, Height
FROM document
WHERE IfcType = 'Wall' AND Label LIKE '%Exterior%'
Count all BIM objects, grouped by their type:
SELECT IfcType, COUNT(*)
FROM document
WHERE IfcType IS NOT NULL
GROUP BY IfcType
ORDER BY IfcType ASC
Get a full quantity takeoff for all walls:
This query calculates the total number of walls, their combined length, and the minimum and maximum wall heights.
SELECT
COUNT(*) AS "Total Count",
SUM(Length) AS "Total Length (mm)",
MIN(Height) AS "Min Height (mm)",
MAX(Height) AS "Max Height (mm)"
FROM document
WHERE IfcType = 'Wall'
List all objects located directly on the "Ground Floor":
This uses the CHILDREN() function to query the contents of a specific container object.
SELECT Label, IfcType
FROM CHILDREN(SELECT * FROM document WHERE Label = 'Ground Floor')
ORDER BY IfcType
List all doors and the specific wall they are hosted in:
This uses the PARENT() function to find the host of each door.
SELECT
Label AS "Door_Name",
PARENT(*).Label AS "Host_Wall_Name"
FROM document
WHERE IfcType = 'Door'
ORDER BY "Host_Wall_Name"
Find all windows on the "Ground Floor" (by checking the grandparent):
This query finds windows, checks their parent (the wall), and then checks that wall's parent (the floor).
SELECT Label
FROM document
WHERE IfcType = 'Window' AND PARENT(*).PARENT(*).Label = 'Ground Floor'
Generate a complete room and area schedule:
This query produces a formatted list of all spaces, grouped by the floor they belong to, with their area converted to square meters.
SELECT
PARENT(*).Label AS "Floor",
Label AS "Room Name",
CONVERT(Area, 'm^2') AS "Area (m²)"
FROM document
WHERE IfcType = 'Space'
ORDER BY "Floor", "Room Name"
When you select a property that has units (a `Quantity`), such as `Length` or `Area`, the BIM Report engine returns the raw numerical value of that property as it is stored internally in the FreeCAD document. FreeCAD's internal units are always based on millimeters (mm).
This means the default output will be in mm for length, mm² for area, and mm³ for volume, regardless of your current unit schema or user preferences. The automatically generated column header will correctly reflect this internal unit.
Example: Default Behavior
If you run this simple query on a space that is 1 meter by 1 meter:
SELECT Label, Area FROM document WHERE IfcType = 'Space'
The resulting spreadsheet will show:
Area (mm²)1000000.0CONVERT()To get the results in the units you desire, you can use the CONVERT() function, which provides explicit control over the output. To obtain the area in square meters, modify the query as follows:
SELECT Label, CONVERT(Area, 'm^2') AS "Area (m²)" FROM document WHERE IfcType = 'Space'
This query now produces the result in the desired unit:
Area (m²)1.0Using CONVERT() guarantees that your report's output is unambiguous and that any calculations you perform within the query (e.g., SUM(CONVERT(Area, 'm^2'))) are mathematically correct.
An Arch Report object exposes the following properties to the user.
Report
SUM(Length) value in the report will update automatically after the recompute. On very large models, it may be desirable to disable this for performance reasons while making many changes. If AutoUpdate is set to False, the report can be updated manually at any time by right-clicking the Report object in the Tree View and selecting Recompute, or by pressing the global Recompute button.The BIM Report engine is fully accessible through Python, allowing you to integrate its advanced query capabilities into your own scripts and automations.
This is the primary function for executing a single, straightforward SQL query. It takes a query string as input and returns a list of the FreeCAD DocumentObjects that match the query. If the query is invalid or finds no objects, it returns an empty list and prints an error to the console.
Example: Get all Wall objects with "Exterior" in their name
import FreeCAD
import Arch
# This query selects all Wall objects whose Label contains the substring "Exterior"
query = "SELECT * FROM document WHERE IfcType = 'Wall' AND Label LIKE '%Exterior%'"
exterior_walls = Arch.selectObjects(query)
# The result is a list of FreeCAD objects
FreeCAD.Console.PrintMessage(f"Found {len(exterior_walls)} exterior walls.\n")
for wall in exterior_walls:
FreeCAD.Console.PrintMessage(f"- {wall.Label} (Length: {wall.Length})\n")
For more complex, multi-step selections, you can use this function to execute a pipeline. It takes a list of ArchSql.ReportStatement objects and returns the final list of FreeCAD DocumentObjects that result from the full pipeline.
This is the programmatic equivalent of using the "Use as Pipeline Step" checkbox in the UI.
Example: Find all doors on the "Ground Floor"
This is a classic hierarchical problem that requires multiple steps. The goal is to find the "grandchildren" (doors) of a "grandparent" (the floor). Since the CHILDREN() function is not recursive, we must chain multiple steps in a pipeline.
'Ground Floor' object.import FreeCAD
import Arch
import ArchSql
# Step 1: Define the pipeline statements.
# The ReportStatement class must be imported from the ArchSql module.
# Statement 1: Find the parent 'Ground Floor' object.
# Output of this step: [Ground_Floor_Object]
stmt1 = ArchSql.ReportStatement(
query_string="SELECT * FROM document WHERE Label = 'Ground Floor'"
)
# Statement 2: Find the direct children of the floor (walls, spaces, etc.).
# is_pipelined=True tells this to run on the output of stmt1.
# Output of this step: [Wall_Object_1, Wall_Object_2, Space_Object_1, …]
stmt2 = ArchSql.ReportStatement(
query_string="SELECT * FROM CHILDREN(SELECT * FROM document)",
is_pipelined=True
)
# Statement 3: Find the children of the objects from the previous step.
# This finds the doors and windows hosted by the walls.
# Output of this step: [Door_Object, Window_Object_1, …]
stmt3 = ArchSql.ReportStatement(
query_string="SELECT * FROM CHILDREN(SELECT * FROM document)",
is_pipelined=True
)
# Statement 4: Filter the final list to keep only the doors.
# This runs on the output of stmt3.
# Final Output: [Door_Object]
stmt4 = ArchSql.ReportStatement(
query_string="SELECT * FROM document WHERE IfcType = 'Door'",
is_pipelined=True
)
# Step 2: Execute the full pipeline.
pipeline_statements = [stmt1, stmt2, stmt3, stmt4]
doors_on_floor = Arch.selectObjectsFromPipeline(pipeline_statements)
# The result is the final list of door objects.
FreeCAD.Console.PrintMessage(f"Found {len(doors_on_floor)} doors on the ground floor.\n")
for door in doors_on_floor:
FreeCAD.Console.PrintMessage(f"- {door.Label}\n")
For debugging, scripting, or sharing your work with others, you can get a complete, human-readable summary of a Report's configuration directly from the Python Console.
The custom logic and detailed content of a BIM Report are stored in its proxy object. Accessing the .Proxy attribute provides a full printout of all statements and their SQL queries. This is a convenient way to copy your entire report definition to the clipboard to share in a forum or a bug report.
How to use it:
obj.print(obj.Proxy)
Example Output:
>>> print(obj.Proxy)
BIM Report: 'My Door Schedule' (2 statements)
--- Statement [1]: Find All Walls on Ground Floor ---
SELECT *
FROM CHILDREN(SELECT * FROM document WHERE Label = 'Ground Floor')
WHERE IfcType = 'Wall'
--- Statement [2]: Final Door Schedule (Pipelined, Header) ---
SELECT
Tag,
COUNT(*) AS "Count"
FROM
document
GROUP BY
Tag