# AutoMATEd: Building CAD Parts and Assemblies with an Iterative LLM Agent

> Source: <https://blog.us.fixstars.com/automated-building-cad-parts-and-assemblies-with-an-iterative-llm-agent/>
> Published: 2026-11-09 22:53:37+00:00

**TL;DR:** Asking a model to write an entire CAD program in one response produced code that had errors or malformed CAD objects. AutoMATEd replaces this one-shot approach with an iterative tool-calling loop inside FreeCAD: the model declares CAD features through a fixed set of 33 validated tools, the harness rebuilds and measures the geometry after every turn, and every failure is fed back until the part is right. The same harness then positions the new part against the existing assembly and can decompose a single request into multiple independently editable parts.

*Can a language model turn one sentence of English into a working 3D model? Yes, but only after we stopped asking it to write CAD programs and started letting it call CAD tools.*

*“Geared turntable carrying an independent slider” – built by AutoMATEd in a single prompt.*

## **The problem we set out to solve**

Mechanical CAD is still largely a manual craft. An engineer who wants a bracket, a shaft, or a gearbox housing spends hours inside a feature tree, sketching profiles, extruding, filleting, and finally repositioning the result against everything already in the assembly. The design knowledge is rarely the bottleneck. What costs time is translating an idea that is already clear in the engineer’s head into a long sequence of exact operations.

That translation step is precisely what a language model ought to be good at. So we set a concrete target: a user describes a part in plain language, and a valid, correctly positioned solid appears in their CAD document with no scripting. The example we kept returning to was a threaded bolt, a washer, and a nut through a plate. For a human that request is trivial. For a model it requires understanding a thread, a clearance hole, a washer face, and how all three stack along one shared axis.

The second half of the problem mattered just as much. Real design is iterative: you describe something, look at it, and then say “make the board thinner” or “shorten the bolt so it sits flush”. A system that cannot survive that follow-up without discarding the model doesn’t truly understand what it’s being asked to build.

## **Why the obvious approach does not work**

The obvious approach is to ask a capable model to write a CAD script. In practice three things have to be true at once before a generated part is usable, and a single-shot response rarely gets all three right.

1. The program has to run. One misspelled method, one wrong argument order, one stray markdown fence, and the entire response is worthless.
2. The geometry has to be valid. Code can execute cleanly and still produce a solid that is empty, inside out, self-intersecting, or shattered into several disconnected pieces.
3. The part has to be positioned relative to other parts per the user’s request. For example, a bolt needs to be contextually built and positioned to fit the L-bracket a user refers to.

The deeper problem is that a one-shot model is blind to its own result. It never learns that the script threw an exception, never sees the degenerate solid, and never notices that the hole is two millimeters too small. All three failures are cheap to detect and cheap to describe. None of them can be acted on without another turn. That asymmetry is the entire argument for an agent.

## **The tools we build on**

We stood on three existing pieces of open infrastructure, each chosen for a specific reason.

### **CadQuery: a Python API over OpenCASCADE**

[CadQuery](https://cadquery.readthedocs.io) is a Python library layered on [OpenCASCADE Technology](https://dev.opencascade.org) (OCCT), the open-source B-Rep kernel also used inside FreeCAD and many commercial systems. You describe a solid as a chain of operations on a workplane, something like “start on the XY plane, draw a circle, extrude it 20 millimeters, cut a smaller hole through it”, and the kernel returns an exact boundary-representation solid that can be exported as STEP.

Two properties made it the right substrate. The output is real engineering geometry, not a mesh approximation: it has exact faces, edges, and analytic surfaces, which is what downstream CAM and simulation tools expect. Second, the API is deterministic, so a wrong result is always explainable, and explainable is the precondition for fixable.

### **FreeCAD: a CAD application that runs our Python**

[FreeCAD](https://www.freecad.org) is an open-source parametric CAD application that supports running Python code as a macro to create objects, set their placement and label, delete them, and refresh the viewport. Today, AutoMATEd docks a panel inside FreeCAD, runs the agent as a separate process, streams its progress into an activity pane, and updates one preview object in the viewport after every tool call.

### **JoinABLe: a learned fallback for mating**

[JoinABLe](https://github.com/AutodeskAILab/JoinABLe) is a pretrained model from [Autodesk’s AI Lab](https://www.research.autodesk.com/) that predicts how two bodies join, trained on the [Fusion 360 Gallery Dataset](https://github.com/AutodeskAILab/Fusion360GalleryDataset). We keep it as a fallback to the Fixstars-native deterministic mating engine we built for the positioning problem.

## **First attempt: let the model write the program**

Our first version did what everyone tries first. We sent the prompt to a model, asked for a complete CadQuery script that assigned the final solid to a variable named `result`, stripped any markdown fences, stripped import statements and executed it in our own process.

The real limitation was structural rather than adversarial: the loop had no return path. When the script failed, and it usually did, the model had no way to find out. The only recovery available was to rewrite the prompt and hope for a better sample.

Across the prompts we tried, the failures clustered into a few recognizable shapes. The model often produced plausible-looking CadQuery that called methods that don’t exist (which is why we stripped import statements). Sometimes it built a profile that did not close (so the extrude returned an empty solid instead of an error) or stacked a head and a shaft as two separate solids that never fused, which passes every line of code and fails the moment you export it. Each of those is a two-line fix if you know about it, and completely invisible if you do not.

None of that is a criticism of the model. The task we gave it was to manage about a hundred interacting details with no opportunity to check its own work, and that isn’t a task shape language models are good at. We were asking for a miracle and grading it as a script.

## **The pivot: stop generating code, start calling tools**

The change that made AutoMATEd work was to stop treating the model as a programmer and start treating it as an operator. Instead of writing a program, the model calls a small, curated set of CAD tools. Instead of producing a script, it produces a feature graph: a list of named features, each with a stable ID, from which the part is rebuilt on every call.

That sounds like a small refactor. In practice it changes the failure profile of the whole system, for four reasons.

- The surface is bounded. The model can only call the operations we implemented and tested. It cannot invent a method, import a library, or reach outside the geometry layer, so an entire class of failures simply cannot occur.
- Arguments are machine-checkable. Every tool validates its inputs before touching geometry: numbers must be finite, vectors must have three components, axes must be non-zero, diameters must be positive. A malformed call is rejected with a precise message instead of producing a broken solid.
- Errors are structured and reusable. A failed tool returns a short, human-readable reason, and that reason goes straight back to the model as the next message.
- Fixes are local. Because features have stable IDs, calling a tool again with the same ID replaces that feature, and removing one deletes it. A wrong hole diameter is one corrected call, not a regenerated program, and a failed rebuild rolls back to the last valid state.

The feature graph is a concrete artefact, not a metaphor. Each feature has a stable ID, so the graph can be edited rather than only appended to, and a failed rebuild rolls back to the last valid state.

The tool set is deliberately broad in shape and narrow in mechanism. Broad, because a mechanical request can call for almost any primitive. Narrow, because each operation should do one predictable thing to the solid rather than expose a general-purpose modeling language.

| **Family** | **Count** | **What it does** | **Examples** | 
|---|---|---|---|
| Additive | 13 | Union a new primitive into the part | box, cylinder, hex prism, extruded profile, thread, bolt, nut, ellipse, spline profile, freeform profile, revolved profile, loft | 
| Cutting | 3 | Subtract material from the part | holes, threaded holes, box cuts | 
| Pattern | 2 | Repeat a named feature | circular array, rectangular grid | 
| Modifier | 4 | Post-process the whole solid | fillet, chamfer, shell, translate | 
| Control | 11 | Manage the session, not the geometry | start a part, name it, select an earlier part, load a part for editing, join, split, inspect, score, finish | 

Additive, cutting, and pattern tools all take explicit world coordinates, not offsets from whatever was built last. The alternative is to chain each feature off the previous one in a local frame: it is more compact to write, but this ends up confusing language models more than it helps because keeping track of relative coordinates causes model hallucination.

## **Keeping the model out of coordinate math**

The single most common source of incoherent output we hit was placement. Early versions let the model reason about where things were relative to each other, and it drifted: a rod perpendicular to a pad it was meant to lie flat on, a boss in the corner of a plate rather than on it, a pattern of holes around the wrong axis. Every one of those is a rotation or translation the model got subtly wrong, and none of them produce an error. They produce a part that looks approximately right, but is completely spatially misaligned with other parts.

Since every placement is an absolute world coordinate in millimeters, the fix was to report the part’s current bounds after each turn, and the harness, not the model, decides how to position parts in space. Local-frame chains are compact and elegant for a human, but coordinates don’t provide enough context for language models to understand.

Two smaller decisions came from the same reasoning. The inspection tool reports real plane normals and cylinder axes rather than a prose description, so the model reads measurements instead of inferring them. And when the inspection notices parts positioned in a misaligned fashion, it provides that information as feedback.

The payoff is that a wrong position is now cheap to describe and cheap to fix. The feedback says which feature is misaligned with which, in coordinates, and the model calls tools to correct each part. What used to cost a full regeneration now costs one tool call.

## **How AutoMATEd works today**

The pipeline has three stages. The agent receives the request, builds the part(s), calls tools to modify the results and then fits the parts together deterministically and/or via JoinABLe. Mating positions the new part against what’s already in the document. Display streams the work into FreeCAD and inserts the final result. The figure below shows how the pieces connect:

*The AutoMATEd pipeline. A prompt enters the tool-calling agent, the feature-graph executor rebuilds/mates parts, validates the solid, and the FreeCAD driver displays the result.*

## **Decomposing one sentence into parts**

The next feature of AutoMATEd is the one that matters most. A request like “a bolt through a block with a washer and nut” is not one solid. It is four, and the distinction is what makes the result editable.

The agent is instructed to treat the request as a decomposition problem. When a request names several components, the model starts a new part for each rather than fusing everything into a single blob, and it provides a semantic label for each one. The label the model assigns is the label that appears in the FreeCAD tree, so the document shows “bolt”, “washer”, and “nut” instead of “generated_part_20260818_001”, so both the language model and the user can understand the design contextually.

Decomposition also runs down the subtree of a nested, single component. If a request asks for a laptop, the agent is implicitly guided to split the laptop into a screen and a keyboard base first, then join the two parts together spatially. And if a user requests to change the laptop’s features, the agent can select the component to modify. A single STEP file can therefore represent several logical parts, and the agent can split one solid into two when the request implies it should. The reverse is available too: parts can be fused, and an existing part can be pulled into the session as the base for an edit.

The result is a two-level structure rather than a flat list. One prompt produces one session, the session holds an ordered set of parts, each part holds a graph of features, and every part is exported as its own STEP file with its own label and its own origin. That origin is the important detail: the session records which existing part, if any, each new part came from, so the system knows whether to insert a new object, replace an existing one, or consume one into another.

The hierarchy is what makes spontaneous editing possible. To change a laptop’s screen thickness, the agent selects the part called “keyboard”, edits that part’s feature graph, and the rest of the session is untouched. Because each part is independent, an edit cannot cascade into parts the user did not ask to change. That property is the difference between a generator and a modeling tool.

See the following examples of different parts AutoMATEd created:

*“a threaded bolt through a block with a washer and nut” — four separate solids, each independently selectable.*

*“a threaded bolt through two thin sheets with a washer and nut” — the same request against a different context.*

## **Closing the loop: three checks the model cannot skip**

A feedback loop is only as good as the signals feeding it, so we added three checks that do not depend on the model’s judgment. They run in Python, in the harness, and the model has no way to skip or misreport them.

The first is a coherence check. After every turn, the harness looks for additive features inside the same part whose volumes genuinely intersect, and it decides that by computing the boolean intersection of the two solids. If the shared volume is zero, they merely touch and the check is silent. If it is greater than zero, the two parts intersect and the model is quoted the intersecting parts. Measured against a real shaft-and-head case, an overlapping head shares 113 mm³ with the shaft and is rejected, while a head seated on the same shaft shares 0 mm³ and is accepted.

Using real volumes instead of bounding boxes is what makes this usable. A nail driven through a bracket is a legitimate mechanical connection, and its bounding box overlaps the bracket completely, but a naive check would reject this formation.

The second check runs at the end of a part. The result must not be empty, must have positive volume, must not collapse to a degenerate bounding box, and must not contain more than one disconnected solid. A single-part request that comes back as five loose solids is a bug, and the model is told to fuse them rather than being allowed to declare victory.

The third is a self-critique. Before accepting a part, the harness shows the model its own tool history, the measured geometry summary, and the full state of the document, and asks whether the result actually satisfies the request. A rejection carries a reason, and the reason becomes the next instruction. The loop is bounded to three rounds so it cannot run forever.

## **Positioning the part**

Generating a good part is half the job. It also has to end up in the right place, which is a different problem with a different solution. Positioning runs in two stages: a deterministic geometric mating engine we built ourselves and fall back on the JoinABLe model if it finds nothing.

The deterministic stage locates the interface between the new part and the existing geometry, computes the transform that seats one against the other, and applies it in a single step. That trade-off is deliberate. JoinABLe is expressive and able to mate arbitrary surfaces, but slow and non-deterministic. On the other hand, the deterministic engine is fast and predictable. We union these two methods to refine our results.

## **Editing instead of regenerating**

Follow-up requests are where generated CAD usually falls apart. If “make the board thinner” causes the system to regenerate the whole assembly from the prompt, the exact geometry the user was looking at is gone and every small accumulated decision with it.

AutoMATEd treats an edit as an edit. The agent can inspect one existing part in detail, including its bounding box, face types, plane normals, cylinders with their radii and axes, and a solid count so it knows whether an imported STEP is one part or a whole sub-assembly. It can then load that part as the base solid, subtract a slab to reduce a dimension, move it, or shorten a feature. On completion the modified part replaces the original object in the document, reusing the original label and placement. To the user it simply looks like their part changed, and nothing else in the model moves.

To test the hierarchical nature of the agent’s generation, we asked the model to create a “threaded bolt” and the following was the result the model generated:

*“threaded bolt” — a complete part from two words.*

This object looks simple, however, it’s composed of a hexagonal head, shaft and threads – all merged together to create a single threaded bolt. The second request was to create a “matching nut for the threaded bolt”. The agent read the context of the threaded bolt, sized the bore to that bolt, and placed the result:

*“matching nut for the threaded bolt” — the second prompt builds on the first.*

## **The models behind it**

Every model in this post is served to our team through [Fixstars Vega](https://www.fixstars.com/en/products-services/vega), our on-premises AI appliance. That choice is not incidental to the work. CAD prompts carry design intent, and Vega keeps both the prompts and the geometry inside our own environment while giving every engineer the same model set and the same flat cost.

We ran the same requests across the open-weight models available through the platform, because the interesting question was whether the harness helps models generally or only a favourite. It helps generally, and the per-model differences were much smaller inside the loop than outside it.

| **Model** | **Role** | 
|---|---|
| [DeepSeek-V4-Pro-0813](https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813) | Primary agent model for the part and assembly loops | 
| [DeepSeek-V4-Flash-0731](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731) | Faster variant, used for latency comparisons | 
| [Qwen3.6-35B-A3B](https://huggingface.co/Qwen/Qwen3.6-35B-A3B-FP8) | Mid-size comparison point | 
| [Qwen3.6-27B](https://huggingface.co/Qwen/Qwen3.6-27B-FP8) | Compact comparison point | 
| [GLM-5.2](https://huggingface.co/zai-org/GLM-5.2) | Strong coding model, text only | 
| [gpt-oss-120b](https://huggingface.co/openai/gpt-oss-120b) | Open-weight comparison point | 
| [Gemma 4 31B IT](https://huggingface.co/google/gemma-4-31B-it) | Vision-capable, reserved for image feedback | 

The last row is the interesting one for what comes next. Everything above it is text-only, which is why the harness communicates through numbers and errors rather than pictures. A vision-capable model in the loop would let the agent look at its own render and critique what it sees, which is a different and much stronger signal than a volume measurement.

## **Results: one-shot prompting versus the iterative feedback loop**

The difference was not incremental. In single-shot mode most attempts failed outright: some responses raised a syntax error and never ran, some produced a program that executed but returned a deformed or incoherent part, and some produced nothing usable at all. Through the harness, every model we tried produced a coherent part.

*Single-shot queries vs. iterative, tool-calling, feedback loop harness, across the open-weight models we tested. In one-shot mode most attempts either failed or produced a deformed part; through the harness, every model produced a coherent one. Each column alternates between one-shot and multi-shot (iterative + tool-usage). Each row is a different prompt the user supplies (i.e. L bracket, threaded bolt, shaft support)*

## **Watch it work**

Two recordings show what AutoMATEd actually feels like to use when hooked up to the Fixstars Vega API. The first is the live workflow inside FreeCAD, from typing a prompt to obtaining the finished result. The second isolates the motion: two separate solids that stay correctly related because a joint says so, rather than because someone positioned them by hand. Each is represented here by a single frame.

### Building a part live in FreeCAD

*One frame from the recording. The prompt goes in, the agent streams its tool calls into the panel, and the part is built and positioned in the document without any manual modelling.*

Watch the clip: [CAD Creation Animation](https://drive.google.com/file/d/1hzTFx9WHKOOa2mMyjr_hkW31xf7vGRlP/view?usp=drive_link)

### Two parts rotating together under a revolute joint

*One frame from the rotating-lever animation. The base plate and the lever arm are independent solids; the revolute joint is the only thing that makes them behave as a single mechanism.*

Watch the clip: [Rotating lever demo](https://drive.google.com/file/d/1QaBKPa79SeIAYounYtJDsHF8npwjwxFO/view?usp=drive_link)

## **What we learned**

- Iterative, error feedback loop engineering – not one-shot prompt engineering – are the key to what makes the agent work. Engineering the feedback loop produced far better results than constructing more detailed prompts. The most valuable output of a tool call is often the exception it raised, which provides the necessary context to prevent the model from guessing.
- Part decomposition hierarchies are key to conveying to an agent what each part does. Splitting a request into independently editable parts is what allows AutoMATEd to understand and build on existing CAD files so engineers can incrementally build CAD files at a drastically lower cost.
- Deterministic checks beat prompting. Anything the harness can verify in Python, it should verify in Python. Asking a model to avoid overlapping features works far less often than measuring the overlap and reporting it.
- Give the model access to tools rather than code. A fixed set of constrained tools beat an unrestricted modeling API, because tools encode semantic intent and validate their own arguments.
- Keep the model out of coordinate math. Absolute world coordinates and deterministic placement allows feedback to be informative to the agent rather than overwhelming.
- Know when not to act. Skipping auto-positioning on edits and multi-part results when it isn’t necessary.

## **FAQ**

### Does the LLM write CAD code?

No. The agent can only call AutoMATEd’s CAD tools, and each call is checked before it changes the model.

### Which models does it work with?

Models that support tool calling through an OpenAI-compatible API. In this post we used GLM-5.2, DeepSeek-V4-Pro, and gpt-oss-120b on Fixstars Vega.

### What does it produce?

A STEP file for every part. Assemblies also come with a bill of materials, and in FreeCAD their joints can be moved and animated.

### Can I try it?

AutoMATEd is a Fixstars-native solution available to our clients. If you want to see how AutoMATEd could improve your CAD workflow or you have any questions/concerns, reach out to our team today!

## **References**

- [CadQuery](https://cadquery.readthedocs.io) : a Python API for building parametric CAD models on OpenCASCADE.
- [OpenCASCADE Technology](https://dev.opencascade.org) : the open-source B-Rep kernel used by both CadQuery and FreeCAD.
- [FreeCAD](https://www.freecad.org) : open-source parametric CAD with a Python macro and scripting interface.
- [JoinABLe](https://github.com/AutodeskAILab/JoinABLe) : learning-based joint prediction for assembly, from Autodesk AI Lab.
- [Fusion 360 Gallery Dataset](https://github.com/AutodeskAILab/Fusion360GalleryDataset) : the joint and segmentation data used to train JoinABLe.
- [Fixstars Vega](https://www.fixstars.com/en/products-services/vega) : the on-premises AI appliance that serves the models used here.

Moving AI onto embedded or edge hardware?

Fixstars engineers port and optimize AI models and embedded software for your target chips, including NVIDIA Jetson and DRIVE, Renesas R-Car, and Qualcomm Snapdragon.

[Request a free assessment](https://www.fixstars.com/en/contact?type=embedded-software-development)

#### Author

##### **Steven Burroughs**

Steven is an AI Research Engineer at Fixstars Solutions. His focus is on engineering AI/ML architectures and data/software solutions. In his free time he enjoys tutoring math, playing the piano and chess.
