-
Notifications
You must be signed in to change notification settings - Fork 68
Creating Skills
This guide explains how to create your own skills for FreeCAD AI. Skills are the primary way to extend the assistant's capabilities with domain-specific knowledge and construction recipes.
Each skill lives in its own directory under <FreeCADAI dir>/skills/:
<FreeCADAI dir>/skills/
my-skill/
SKILL.md # Required: LLM instructions
VALIDATION.md # Optional: geometry validation rules
handler.py # Optional: Python handler
references/ # Optional: additional docs loaded on demand
dimensions.md
materials.md
The directory name becomes the slash command. A directory named mounting-bracket creates the command /mounting-bracket.
Requirements:
- The directory must contain a
SKILL.mdfile. Directories without this file are ignored. - The directory name should be short, lowercase, and hyphen-separated (e.g.,
thread-insert,snap-joint,cable-clip). -
handler.pyis optional. If present, it must contain anexecute(args)function. -
references/is optional. Use it for detailed reference data (dimension tables, material properties) to keep SKILL.md concise.
The SKILL.md file is the core of every skill. When the user invokes the skill, this file's contents are injected into the LLM's system prompt. The LLM then follows the instructions to build the model using tool calls.
Skills can include YAML frontmatter at the top of SKILL.md to provide metadata:
---
name: my-skill
description: Create parametric mounting brackets with configurable hole patterns.
---
# My Skill
Construction instructions here...The description field is preferred over body-text extraction and supports longer, more descriptive text. This description appears in the system prompt so the LLM knows what skills are available. The name field is informational (the directory name is always used for the trigger command).
If no frontmatter is present, the description is extracted from the first non-empty, non-heading line of the SKILL.md body.
Skills use a layered loading system to manage context size:
- Name + description — always visible in the skills list (~10 words)
- SKILL.md body — loaded when the skill is invoked (keep under 200 lines)
- References — loaded on demand when the skill tells the LLM to read them
If your skill needs extensive reference data (dimension tables, material properties, multi-variant instructions), put it in references/ and point to it from SKILL.md:
For standard metric thread dimensions, read `references/thread-tables.md`.A good SKILL.md follows this structure:
---
name: skill-name
description: One-line description of what this skill creates.
---
# Skill Title
One-line description of what this skill creates.
## Parameters to extract from user request
- **param1**: description (default value)
- **param2**: description (default value)
## Construction steps
### 1. First step
- Tool calls and their parameters
### 2. Second step
- Tool calls and their parameters
## Important notes
- Gotchas, warnings, tolerances
## Reference data
- Lookup tables, standard dimensionsBe specific about FreeCAD operations. Name the exact tool, feature type, and property. Do not write "extrude the shape" -- write pad_sketch with the sketch name, length, and body_name. The LLM needs unambiguous instructions.
# Good
- `create_sketch` on XY, body_name="Base": rectangle x=0, y=0, width=L, height=W
- `pad_sketch` length=H, body_name="Base"
# Bad
- Create a rectangle sketch and extrude it to the desired heightInclude default values. Users should be able to invoke the skill with minimal arguments. Define sensible defaults for every parameter and document them clearly.
## Parameters
- **Length**: outer length in mm (required)
- **Wall thickness**: default 2mm
- **Fillet radius**: default 1mm (0 = no fillet)Add dimensional reference tables. If your skill involves standard sizes (bolt holes, bearing bores, pipe diameters), include a lookup table so the LLM does not have to guess.
| Bearing | Bore | OD | Width |
|---------|------|------|-------|
| 608 | 8mm | 22mm | 7mm |
| 6001 | 12mm | 28mm | 8mm |
| 6201 | 12mm | 32mm | 10mm |Warn about FreeCAD pitfalls. Document issues the LLM is likely to encounter:
- Pocket sketches must use
offset=Hto cut from the top face (otherwise no floor) - Boolean operations fail on coplanar faces
-
Revolutioncrashes if the profile crosses the axis - Always pass explicit
lengthtopad_sketch(do not rely on the 10mm default) - Always pass
body_namewhen multiple bodies exist
Keep it under 200 lines. Long skill files dilute the LLM's attention. If your skill needs extensive reference data, put it in references/ files or use a handler.py for lookup logic.
Use the actual tool names. The LLM maps skill instructions to tool calls. Reference the exact tool names from the Tool Reference: create_body, create_sketch, pad_sketch, pocket_sketch, revolve_sketch, loft_sketches, sweep_sketch, boolean_operation, transform_object, fillet_edges, chamfer_edges, execute_code, etc.
Here is a complete example for a skill that creates a box with rounded edges:
# Rounded Box
Create a rectangular box with filleted edges.
## Parameters to extract from user request
- **L**: length in mm (required)
- **W**: width in mm (required)
- **H**: height in mm (required)
- **R**: fillet radius in mm (default 2mm)
## Construction steps
### 1. Create body
- `create_body` label="RoundedBox"
### 2. Base sketch and pad
- `create_sketch` on XY, body_name="RoundedBox":
rectangle x=0, y=0, width=L, height=W
- `pad_sketch` length=H, body_name="RoundedBox"
### 3. Fillet all edges
- `measure` with measure_type="edges" on the Pad to get edge names
- `fillet_edges` on the Pad, select all 12 edges, radius=R
## Important
- Always measure edges before filleting -- edge numbering depends on geometry
- If R is too large (> min(L,W,H)/2), FreeCAD will fail. Warn the user.
- Fillet radius must be > 0. If user requests R=0, skip step 3.A handler adds Python logic that runs before the SKILL.md is injected. Use a handler when your skill needs deterministic computation that the LLM should not be left to guess.
| Use Case | Example |
|---|---|
| Calculations | Computing involute gear profiles, thread pitch geometry |
| Lookup tables | Selecting bearing dimensions from a catalog |
| Parameter validation | Checking that wall thickness is not larger than the box |
| File I/O | Reading a template file, generating a BOM |
| Argument parsing | Extracting structured parameters from free-form text |
Every handler must define an execute(args) function:
def execute(args):
"""Process skill arguments and return a result.
Args:
args: Raw string of everything the user typed after the /command.
For example, if the user types "/my-skill M3 at (10,10)",
args will be "M3 at (10,10)".
Returns:
dict with one of these keys:
{"inject_prompt": "text"} -- inject text into the LLM prompt
{"output": "text"} -- display directly to the user
{"error": "text"} -- show an error message
Can also return a plain string (treated as {"output": string}).
Return None to fall through to SKILL.md injection.
"""| Return | Behavior |
|---|---|
{"inject_prompt": "text"} |
The text is injected into the LLM's system prompt instead of (or in addition to) the SKILL.md content. The LLM then processes the injected text. |
{"output": "text"} |
The text is displayed directly to the user. The LLM is not involved. |
{"error": "text"} |
An error message is displayed to the user. |
None |
The handler is skipped and the SKILL.md content is injected as usual. |
A handler that parses thread insert arguments and computes the correct hole dimensions:
"""Handler for thread-insert skill with parameter lookup."""
# Standard heat-set insert dimensions: (hole_diameter, depth, clearance_diameter)
INSERT_TABLE = {
"M2": (3.2, 3.5, 2.4),
"M2.5": (3.6, 4.0, 2.9),
"M3": (4.0, 5.0, 3.4),
"M4": (5.6, 6.0, 4.5),
"M5": (6.4, 7.0, 5.5),
}
def execute(args):
"""Parse insert size from args and inject computed dimensions.
Args:
args: e.g. "M3 at four corners" or "M4 depth=8"
Returns:
dict with inject_prompt containing the SKILL.md plus computed values,
or an error if the size is not recognized.
"""
# Extract the metric size (M2, M2.5, M3, M4, M5)
size = None
for token in args.upper().split():
if token in INSERT_TABLE:
size = token
break
if size is None:
return {"error": f"Unrecognized insert size. Supported: {', '.join(INSERT_TABLE.keys())}"}
hole_d, depth, clearance_d = INSERT_TABLE[size]
prompt = (
f"Create {size} heat-set insert holes.\n"
f"Insert hole diameter: {hole_d}mm\n"
f"Insert depth: {depth}mm\n"
f"Clearance hole diameter (if through-hole needed): {clearance_d}mm\n"
f"\nUser request: {args}\n"
)
return {"inject_prompt": prompt}Handlers are loaded dynamically using importlib.util.spec_from_file_location(). The handler module is loaded fresh each time the skill is invoked, so changes to handler.py take effect immediately without restarting FreeCAD.
If the handler raises an exception, the error is caught and returned as {"error": "Skill handler error: <message>"}. The SKILL.md is not injected as a fallback -- the error is reported to the user.
If the handler's execute() function returns None or does not exist, the system falls through to injecting the SKILL.md content as the prompt.
The fastest way to create a skill is to use the built-in /skill-creator meta-skill:
/skill-creator I need a skill for creating PCB standoffs with M3 inserts
The /skill-creator follows an interview-and-iterate approach:
- Capture intent — understand what the user wants, extract what it can from context
- Interview for details — ask about parameters, edge cases, construction approach, standard dimensions, and FreeCAD pitfalls (skipping questions already answered)
- Choose a name — pick a short, hyphenated name and confirm with the user
-
Write the skill — generate SKILL.md (with YAML frontmatter) and optionally handler.py, using progressive disclosure (
references/for large data) -
Save the files — create the skill directory using
execute_code - Test and iterate — propose 2-3 realistic test invocations, run them, evaluate results, and improve the skill based on feedback
The iterative testing loop is a key part of skill creation. After each test run, the LLM checks results with get_document_state and measure, notes what worked and what didn't, and asks for user feedback before improving.
After the skill is created, you can immediately invoke it with the chosen command name.
Skills can optionally include a VALIDATION.md file that defines expected geometry properties. This enables two features:
-
--validateflag -- users can append--validateto any skill invocation to check the result -
Optimizer scoring -- the
/optimize-skillcommand uses validation rules to measure geometry correctness
my-skill/
SKILL.md
VALIDATION.md # Optional: geometry validation rules
handler.py # Optional: Python handler
# Validation Rules
## Parameters
L: float # required parameter
W: float
H: float
T: float = 2 # optional with default
lid_type: str = screw # string parameter
## Checks
### Body count
- total_bodies: 2
### MyBody
- exists: true
- bbox: L, W, H (tolerance 0.5)
- volume: L*W*H - (L-2*T)*(W-2*T)*(H-T) (tolerance 5%)
- solid_count: 1
- valid_solid: true
#### when lid_type == "screw"
- min_children: 4| Type | Widget in optimizer dialog | Example |
|---|---|---|
float |
Spin box (decimal) | L: float |
int |
Spin box (integer) | count: int = 4 |
str |
Dropdown (if known values) or text field | lid_type: str = screw |
bool |
Checkbox | add_vents: bool = false |
| Check | Description | Value format |
|---|---|---|
exists |
Object with this label exists | true |
bbox |
Bounding box dimensions match |
X, Y, Z (tolerance N) -- absolute mm |
bbox_position |
Absolute Z position of object |
Zmin, Zmax (tolerance N) -- catches misplaced parts |
section_area |
Cross-section area at a given position |
axis, offset, area (tolerance N%) -- catches flipped/inverted parts |
volume |
Volume matches formula |
expression (tolerance N%) -- relative % |
solid_count |
Number of solids in shape | integer |
valid_solid |
Shape is valid with at least 1 solid | true |
total_bodies |
Count of PartDesign bodies in document | integer |
has_holes |
Count of through-all pockets | integer |
has_feature |
Named feature exists in body | "FeatureName" |
min_children |
Minimum feature count in body | integer |
Use #### when param == "value" to add checks that only apply for specific parameter values. The when block is scoped to its parent ### ObjectName section.
Volume and bbox values use a small, safe arithmetic language. Expressions are parsed via Python's ast module -- no code is ever executed, only arithmetic is evaluated from the syntax tree.
| Category | Syntax | Example |
|---|---|---|
| Arithmetic |
+, -, *, /
|
L - 2*T |
| Power | ** |
PR**2 (PR squared) |
| Modulo | % |
L % 10 |
| Parentheses |
( )
|
(L - 2*T) * (W - 2*T) |
| Unary minus | - |
-T |
| Constants | pi |
pi * R**2 |
| Functions |
sqrt(), abs(), min(), max()
|
sqrt(L**2 + W**2) |
| Parameters | Any name from ## Parameters
|
L, W, H, T
|
# Rectangular shell (4 walls + floor, open top)
L*W*H - (L-2*T)*(W-2*T)*(H-T)
# Shell + 4 cylindrical screw posts
L*W*H - (L-2*T)*(W-2*T)*(H-T) + 4*pi*PR**2*(H-T)
# Circle area
pi * R**2
# Diagonal of a rectangle
sqrt(L**2 + W**2)
# Lid with lip
L * W * T + (L-2*T-0.4) * (W-2*T-0.4) * 3
The expression language is intentionally limited for safety. The following are not supported and will cause a parse error:
- String operations -- no concatenation, no string methods
-
Comparisons -- no
<,>,==,if/else(usewhenblocks for conditional logic) -
Attribute access -- no
obj.attr, nomodule.function() -
Imports -- no
import, no__import__ -
List/dict operations -- no
[], no{} -
Any Python built-in besides
sqrt,abs,min,max-- nolen(),sum(),range(), etc. -
Variables not declared in
## Parameters-- all variable names must be listed -
^for power -- use**instead (^is bitwise XOR in Python)
If you need more complex logic (conditionals, lookups), use #### when blocks to split the check into separate conditional rules, each with its own simple expression.
-
Absolute:
(tolerance 0.5)-- within 0.5mm -
Relative:
(tolerance 5%)-- within 5% of expected value
For --validate to work during normal use, your SKILL.md must instruct the LLM to call report_skill_params at the end:
## Critical rules
- After completing ALL construction steps, call `report_skill_params` with the parameters used: L, W, H, T, and lid_type.The optimizer does not need this -- it already has the parameters from the dialog.
Append --validate to any request that uses a skill:
make me an enclosure 100 80 40 snap-fit --validate
After the skill completes, validation results are shown in the chat:
Validation: 8/9 checks passed
✓ total_bodies: 2
✓ EnclosureBase exists
✓ EnclosureBase bbox: 100.0 x 80.0 x 40.0
✓ EnclosureBase volume: 38847.3 (expected 38400.0 ±5%)
✓ EnclosureBase solid_count: 1
✓ EnclosureBase valid_solid
✗ EnclosureLid bbox: expected 100.0 x 80.0 x 5.0, got 100.0 x 80.0 x 2.0
✓ EnclosureLid solid_count: 1
✓ EnclosureLid valid_solid
After creating a skill, verify it works:
-
Invoke the skill by typing the
/commandin the chat input. - Check that the LLM follows the instructions -- watch the tool calls in the chat output. Verify the operations match your SKILL.md steps.
- Inspect the result -- rotate the 3D view, check dimensions, verify all features are present.
- Test with different parameters -- try edge cases (very small dimensions, zero fillet radius, maximum values).
- Iterate on the SKILL.md -- if the LLM misinterprets a step, make the instructions more explicit. If it skips a step, add emphasis or reorder.
Common issues and fixes:
| Problem | Fix |
|---|---|
| LLM skips a step | Add "IMPORTANT:" prefix or bold the step |
| LLM uses wrong dimensions | Add explicit formulas with variable names |
| LLM confuses body names | Add body_name= to every tool call in the instructions |
| LLM forgets to pass length to pad | Add "ALWAYS pass explicit length" to critical rules |
| Features go into wrong body | Ensure every step specifies the target body_name |
Skills are fully portable. To share a skill:
-
Copy the skill directory (e.g.,
<FreeCADAI dir>/skills/my-skill/) to a zip file, git repo, or file share. -
The recipient copies the directory into their own
<FreeCADAI dir>/skills/. - Restart FreeCAD (or close and reopen the chat panel) to reload the skills registry.
Skills have no external dependencies beyond FreeCAD itself. The SKILL.md is plain Markdown and handler.py uses only Python stdlib (plus any FreeCAD modules if called within FreeCAD).
Distributing via git:
# Share your skill
cd <FreeCADAI dir>/skills/my-skill
git init && git add . && git commit -m "Initial skill"
git remote add origin https://github.com/user/freecad-skill-my-skill
git push -u origin main
# Install someone's skill
cd <FreeCADAI dir>/skills
git clone https://github.com/user/freecad-skill-my-skill my-skillNext: Skills | Skills Reference | Tool Reference