Skip to main content
This is 2026.9 documentation. Coverage here is incremental: a page exists only where 2026.9 differs or is newly documented. For any other topic, switch to Current in the version selector.
This page covers what changes for GraphQL and REST consumers in 2026.9 — scripts, low-code flows (Power Automate, Zapier, n8n), and custom integrations that call the API directly. It is not a full reference: for everything unaffected, keep using the current GraphQL and REST documentation. For the conceptual model behind this change and who’s affected, see Object model in 2026.9 and Data model migration for developers.
Sending a 2026.9 version’s referencedObject to the legacy objects endpoints (/objects/ {projectId}/{objectId}, Project.object(id)) returns 404 with error: "BUNDLE_REFERENCE_NOT_FOUND". Treat that as “this version uses the new format,” never as “empty model.” You can tell which format a version uses from the shape of referencedObject itself — see the table below.

Tell the two formats apart

Workspaces in compatibility mode — sometimes called “legacy conversions” or “legacy sync” in support conversations, same thing — can still produce the first shape for uploads and ACC sync, until 1 November 2026. See Data model migration for developers for how it’s granted. Check the shape of referencedObject on the version you actually have — don’t assume based on upload date or workspace settings alone.

What’s no longer supported

For a version whose referencedObject is a bundle reference, these stop working. There is currently no GraphQL-level replacement for the filtering and selection behavior — see Get element data today for what to use instead.
  • Object.children(query:, select:, limit:) — the server-side filter/select mechanism (for example, filtering by an IFC type and selecting a handful of property paths in one request).
  • Object.totalChildrenCount — nothing to count against; there is no object tree.
  • Project.object(id: referencedObject) — the reference isn’t a content hash, so there’s nothing to look up.
  • Reading geometry as JSON (the data field) — 2026.9 geometry is stored in a binary format read by the Viewer and SDKs, not served as a JSON payload.

Get element data today

Prior to 2026.9, one GraphQL query could filter elements by property and select just the fields you needed:
Prior to 2026.9
Variables
The 2026.9 call returns every indexed property on every element, one row per property, rather than a pre-filtered result:
To reproduce the “every IfcDoor” filter from the query above, filter that array yourself:
1

Find matching object ids

Filter the rows for path == "ifcType" and valueText == "IfcDoor", and collect their objectId values. The outcome is the id of every door in the model.
2

Pull their other properties

Filter the full array again for rows whose objectId is one of those ids. The outcome is every property row belonging to a matching element.
3

Group by objectId

Group the result by objectId to get one record per door with its properties. The outcome matches what select used to return, without the size limit.
In a low-code tool, that’s two filter steps in sequence — two Filter array actions in Power Automate, two Filter nodes in n8n — no custom code, just more steps than the single GraphQL query it replaces. There’s also a GET .../versions/{versionId}/eav/download variant for a plain download, and a GET .../models/{modelId}/eav/download variant that always serves the latest version.
This endpoint doesn’t accept filter parameters yet — it always returns the whole properties table, and you filter after receiving it. That’s fine at door-count scale; less fine on a model with hundreds of thousands of elements.

Payload size, and moving to the bundle directly

Losing select and query doesn’t break anything — you still end up with the same elements once you’ve filtered client-side — but it does change what “minimal” means. The old query returned exactly the fields you asked for, for exactly the elements that matched. The properties endpoint above always returns everything: every indexed property, on every element, every time you call it. That’s a fixed cost that scales with model size, not with how much of the model you actually wanted. For a one-off check, or a model with a few hundred elements, that’s a non-issue. For something that runs repeatedly against a large model — a nightly flow, a live dashboard — downloading and re-filtering the full properties table on every run is the wrong shape for the job, even though it technically works. At that point, don’t look for a lighter version of the JSON export — there isn’t one yet (see the FAQ below). Instead, stop treating the version as “a JSON payload to filter” and start reading its bundle directly:
This returns presigned URLs to the version’s actual parquet files — the same underlying data the properties export flattens into JSON, but structured for reading only the columns and rows you need instead of pulling the whole table every time. This is what the SDK paths below attach to directly, which is why they scale where the JSON export doesn’t.
This only helps if your integration can run code that reads Parquet — it isn’t a fit for most low-code tools, which is exactly the properties-export endpoint’s use case instead. Self-hosted n8n is a partial exception; see the FAQ below.

From an SDK instead of raw HTTP

For anything that can run real code rather than a low-code flow, an SDK’s native path reads the bundle above directly instead of downloading the full properties table yourself:

FAQ

Probably not. If the failure happens at upload time — a 400 error, code UNSUPPORTED_FILE_TYPE — you’re almost certainly still calling the deprecated startFileImport mutation, which the 2026.9 server rejects outright as of 1 September 2026. See File uploads to switch to startFileIngestion. This page covers a different symptom: uploads that succeed, but querying elements back afterward returns null or empty.
The version you queried is in the 2026.9 format — check whether referencedObject is a bundle. reference (see Tell the two formats apart). This isn’t a bug or a temporary gap: server-side filtering over the object tree doesn’t apply to that format. Use the properties endpoint above instead.
Yes, unchanged. Nothing on this page affects a version whose referencedObject is a hex string. The two formats can coexist — check the version you actually have rather than assuming based on project or workspace.
Probably not. A version’s id is reserved as soon as ingestion starts, but the version — and its properties — don’t exist until ingestion actually succeeds, which can take from several seconds to around a minute depending on the file and rail. Querying immediately after upload will legitimately show nothing. Poll Model.latestIngestion.statusData until it reports ModelIngestionSuccessStatus, then query the version it names — don’t fire the read on a timer or a webhook that isn’t actually tied to ingestion completion.
No — it works for any version that already has a properties artifact built, hex-referenced or bundle-referenced. It 404s with "Version not found or has no EAV artefact" if that version hasn’t had one built yet, which is a different failure than “wrong format.” Bundle versions always have one; older versions may or may not, depending on whether they’ve been through backfill.
Not documented yet. This page will be updated when that changes. For now, download and filter client-side.
Compatibility mode affects new uploads and ACC sync going forward; it doesn’t change versions that already exist. Confirm you’re querying the version created after compatibility mode was turned on, and that its referencedObject is a hex string, not a bundle. reference. See Data model migration for developers — compatibility mode ends 1 November 2026 either way, so plan the move regardless.
Not available today for 2026.9 versions — count distinct objectId values in the properties export, or use an SDK’s native accessors (see From an SDK instead of raw HTTP).
Not natively, in most cases. Power Automate and Zapier have no built-in way to parse Parquet — that’s a platform gap, not something Speckle can route around. n8n is a partial exception: its Code node can’t import external npm packages on n8n Cloud, so the same limitation applies there, but a self-hosted n8n instance can read Parquet if you enable external modules (NODE_FUNCTION_ALLOW_EXTERNAL) or install a community node built for it — that’s a deliberate setup step, not the default. The properties export endpoint exists specifically so low-code tools have a plain-JSON option that works everywhere, cloud or self-hosted, with no extra configuration. See Payload size, and moving to the bundle directly.
Two questions decide it: can your integration run code that reads Parquet, and does it run repeatedly against a large model? If either answer is no — a low-code flow, a one-off check, a small model — use the JSON properties export; it’s simpler and the overhead doesn’t matter. If both are yes — a script or service running on a schedule against a model with many thousands of elements — read the bundle directly instead. See Payload size, and moving to the bundle directly.
Yes. Format is decided per version, at the time it’s created, not per project or workspace. Older versions and versions made while compatibility mode was active can be hex-referenced while newer versions on the same model are bundle-referenced. Always check the version you’re actually reading, never assume from a sibling version or from when the project was created.
Last modified on September 7, 2026