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 (2026.8 and earlier) in the version selector.
If you connect a host application or a service to Speckle with your own code rather than a Speckle SDK, 2026.9 changes what you write and read. There is no root object to serialize and no object graph to walk. A version is a bundle, a set of parquet files, and a model ingestion is how the server turns your upload into a version. This page introduces that model and points to the two flows built on it. It is the 2026.9 successor of Data model and build order.
These pages are for people who build connectors, sync services, and other producers or consumers. If you write scripts with a Speckle SDK, the SDK applies every rule here for you. Start from the .NET or Python 2026.9 pages instead.

Publish

Open an ingestion, write and upload a bundle, and wait for the version. The wire sequence, the correctness rules, and a build order.

Load

List a version’s artifacts, download the files you need, and rebuild objects, geometry, instances, grouping, and appearance from the bundle.
Decide publish only or publish and load for your host before you start, the same way as today. See Publish and load.

What changes

What a bundle builder is

Every Speckle producer has a component that plays the role the SDKs call a bundle builder. In specklepy and Speckle.Sdk it is BundleBuilder. The Archicad connector implements the same role in C++ without an SDK. It is the producer-side model of a version, and it has four jobs.
  • Intern identities into dense integer keys. Objects are keyed by applicationId. Geometry is keyed by a value you derive from the owning object, such as wall-1:g0. Structural nodes such as definitions, instances, materials, colors, levels, and grouping containers are keyed by whatever stable key your host gives them. Each of the three key spaces counts from zero on its own. A relation’s type is what says which space each end refers to.
  • Record typed relations. Every link is one row of (relation, source, target, ord). Membership, hosting, connectivity, appearance, and instancing are all relations, never nested fields.
  • Stream rows to parquet as you add them. Geometry is encoded on the spot and flushed in row groups, so memory does not grow with the model.
  • Stamp provenance. The builder writes the meta row and the self-describing catalogs from the bundle spec’s generated constants, never from literals you typed in.
The builder’s behaviour is the contract, whichever language you write it in:
  • Get-or-add is interning. Asking for a key that already exists returns the same handle and writes nothing. Asking for it again with different attributes is an error. One key means one node.
  • Add is append. Geometry, model properties, results, and camera views append a row on every call.
  • Edges cannot be retracted. A single-valued relation is write-once. Decide before you write.
  • Units and producer are required up front. The default unit is stamped on every object, and the producer slug and version go into meta and onto the ingestion.
  • A default scene view is injected when you declare none, grouping by IN_COLLECTION.
  • Build once, then rename. The builder writes files under a temporary stem and renames them to the reserved version id once the ingestion hands it out.

What a bundle contains

A bundle is a flat set of files sharing one stem, the version id, with no manifest file. Consumers know the filenames from the spec and discover the stem from the meta file. Two files you do not write:
  • The viewer .dat. The server builds {id}.viewer.dat from your bundle after completion. That build is part of why the version is not visible the moment the upload completes.
  • A manifest or index. The meta file is the anchor. Everything else is found by name.
Optional files are feature-detected by presence. An empty optional table is a bug, not a placeholder. The three type tables are the exception: the viewer needs all three present to build its property view, so write them even when they hold no rows.

Relations you do not need, and relations you add

Two questions come up as soon as a team maps its own domain onto the relational layer. Both have a short answer. You do not have to emit every relation type. The catalog describes what Speckle can carry, not what every model must contain. A producer for a host with no levels, no instances, or no MEP systems emits none of those relations, and a consumer treats the absence as “not in this model”, never as an error. Emit what your domain has and nothing else. You can add your own links, with the same guarantee interoperability has always had: none. The relation catalog is Speckle’s. Its numbers are reserved or retired on purpose, the validator rejects a relations file that uses a number outside the catalog, and every Speckle consumer ignores edges it does not know. Custom links therefore go in your own parquet file, uploaded with the bundle under the same version id, keyed by applicationId so they survive across versions. Speckle stores and lists that file and never reads it. The viewer, Speckle Intelligence, and the SDK receive path will not see those links, and a receiving application supports them only if you wrote that support. Say so in your own documentation. Before you add a file, check which of these three shapes your case is. Most turn out to be the first two.
  • A relation Speckle already has. A structural “supports” link, a duct run, a pipe network: CONNECTS_TO with a scope, IN_SYSTEM, or SUBELEMENT usually carries it, and then every consumer understands it. Read the relation catalog first.
  • A grouping, not a link. A classification hierarchy, a cost breakdown, a zone scheme: put the code on each object as a property and declare a scene view over that property path. The viewer then offers your hierarchy as a tree without any new relation.
  • A link to something outside the model. An issue, a clash, a record in an asset register: when the other end is not an object in the bundle, an id on the object as a property is the portable form. Use a custom edge file only when both ends are objects in the same bundle.
If a link is general to a discipline rather than to your tool, propose it as an issue on speckle-bundle-spec with the two node kinds it joins and an example model. Reserved catalog numbers exist so that new relations can be added for everyone, and a relation in the catalog is the only kind every consumer will understand.

Open-source producers to learn from

Every Speckle connector is open source. Read them as worked examples of the publish and load rules, in the language closest to yours. The SDKs are the reference builders; the non-SDK producers show the same contract implemented from scratch. The user interface is separate from publishing. The shared connector UI, speckle-connectors-dui (TypeScript), is what the .NET desktop connectors, Archicad, and SketchUp embed. Using it is optional. It is worth reading for two things: the bindings show the read and write interactions a host has to expose, including how the UI follows an ingestion until the version exists, and reusing it keeps the experience familiar to people who already use other Speckle connectors. Blender ships its own native panels instead. Grasshopper has no UI at all and splits publish and load into components. Navisworks has no 2026.9 desktop connector. Large NWC and NWD files go through direct uploads, where a server-side converter produces the bundle. Older archived repositories such as the QGIS, Unity, and Excel connectors publish the object graph of 2026.8 and earlier and are not a pattern for 2026.9.

FAQ

Yes. It lives at speckle-bundle-spec: the SQL source that defines every table, relation, and node kind, with the reasoning recorded beside each entry, plus the code generators, a validator, and conformance fixtures. Generated constants for C++, C#, Python, and TypeScript are under generated/. Pin to a commit, build against the constants for your language, and run the validator on every bundle in CI. To propose a relation, open an issue there with the two node kinds it joins and an example model.
Emit nothing for them. An absent relation means “not in this model”. Consumers must never treat a missing relation type as an error.
On every object as a root scalar, on every instance node, in every SGEO header, and per property row where a value has a unit. There is no bundle-wide unit.
Last modified on September 16, 2026