What changed
Creating a version used to be synchronous: the mutation wrote the version, and you could query it, open its URL, or receive it immediately. Now every version-creating call goes through the same processing pipeline as file uploads, called a model ingestion:- Your call returns immediately with a reserved version id. The response shape is unchanged, so existing code keeps compiling and running.
- The server processes your already-uploaded objects in the background (packing, then building the new-format data bundle).
- The version is created when processing completes. Only then does it appear in queries and version lists, resolve at its URL, and fire
version_createdwebhooks and subscriptions.
SendPipeline in the .NET SDK.
This applies to app.speckle.systems and to self-hosted servers running 2026.9 or later. It is
part of the wider data model migration.
Workspace compatibility mode changes the conversion path used by file uploads and cloud syncs. It
does not change object sends completed through
version.create or commitCreate; the server
still packs and converts those sends into a bundle before creating the version.What to call instead
specklepy scripts
client.version.create is deprecated but still accepted. It returns the same Version object, but its id is a reserved id: the version does not exist yet.
BundleBuilder and publish it with operations.send3, which runs on the ingestion rail directly and skips the server-side conversion. See Publish a bundle from your own code.
.NET scripts (Speckle.Sdk)
client.Version.Create is deprecated but still accepted in the same way: the returned Version.id is reserved, not yet queryable.
The supported successor is the ingestion upload path the desktop connectors use: create an ingestion, run SendPipeline, and let the server create the version when processing completes. The ingestion is your progress and readiness surface, and there is no separate Version.Create call at all.
Example
client.Subscription.CreateProjectModelIngestionUpdatedSubscription for push-based updates instead of polling.
If you build the bundle yourself with a BundleBuilder, Send3 publishes it over the v2 upload rail (uploads/complete), and WaitForVersion returns once the version exists. See Publish a bundle from your own code. Integrations that already produce a Base tree can keep using SendPipeline and let the server do the conversion.
Raw GraphQL and v1-era scripts
BothversionMutations.create and the legacy commitCreate mutation (still used by SketchUp-era tooling and v1 scripts) return the id synchronously. Treat it as reserved: do not build “view it online” links, query the version, or hand the id to another system until the ingestion completes. Use the queries and subscription in Wait for the version.
Automate functions
CreateNewVersionInProject (.NET) and create_new_version_in_project (Python) keep their signatures in the 2026.9 Automate SDKs (specklepy[bundle]==2026.9.0b3, Speckle.Automate.Sdk 2026.9.0-beta.4 or later). The returned id is reserved:
Example
version_created are unaffected: the trigger already fires only when the version exists. Your function is only affected if it reads back a version it just created in the same run. In that case, wait for it as shown below. In .NET, CreateNewVersionInProject also accepts a BundleBuilder and publishes it over the ingestion rail; call context.WaitForVersion on its result before the run ends.
Wait for the version
The ingestion is the readiness surface while your version is being processed. Its lifecycle:queued→processing→success,failed,cancelled,timeout, orinvalidInput- While
processing, legacy sends report the server-side phasesPACKING(raw objects into a packfile) andBUNDLING(packfile into the new-format bundle), with progress. - On
success, the status carries the version id, and the version now exists. - On any failure status, no version is ever created. Fix the problem (or just retry) and resend.
ModelIngestion.versionId(top-level field): the reserved id, present from the moment the ingestion is created. It matches the id your create call returned. Use it to find your ingestion, never to decide the version exists.ModelIngestionSuccessStatus.versionId(insidestatusData): only present once the ingestion succeeded. This is the signal that the version exists.
model.ingestionHistory instead of latestIngestion and pick the entry whose versionId matches yours.
For push-based updates instead of polling, use the projectModelIngestionUpdated subscription with a modelId reference and react when statusData becomes a success or failure status. See Real-time subscriptions for a complete example.
version_created webhooks also fire only at completion, so existing webhook consumers need no change: by the time your handler runs, the version is queryable.
No removal date
No removal date is set for legacy send. Retirement will be criteria-driven and announced well in advance. Until then, servers with legacy-send support accept the deprecated calls, provided your code handles the deferred version visibility described above. Build new work on bundles: a server without legacy-send support already rejects these calls withLEGACY_SEND_UNSUPPORTED.
FAQ
Why does reading a version right after creating it fail?
Why does reading a version right after creating it fail?
The id you got back is reserved, not yet a real version. Add a wait as shown in Wait for the
version.
Should I still publish with operations.send?
Should I still publish with operations.send?
Not for new work.
operations.send still uploads objects, and only the version-creating step
became asynchronous, but publishing an object graph is deprecated. New publishes build a bundle
and use operations.send3.How do I know a publish failed?
How do I know a publish failed?
The ingestion ends in a failure status (
failed, cancelled, timeout, or invalidInput) and
no version is created. Polling the version alone cannot distinguish “still processing” from
“failed”, so check the ingestion status when a wait runs long.What happens if I parse the success status off completeWithVersion's response?
What happens if I parse the success status off completeWithVersion's response?
It breaks: that mutation now returns the ingestion in
processing with the reserved id on the
top-level versionId field, not a success status. Read the reserved id from there, then wait
for the ingestion to succeed. The mutation was already deprecated; new uploads should complete
through the v2 upload rail.