Skip to content

Creating Skills

alf edited this page May 1, 2026 · 7 revisions

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.

Skill Directory Structure

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.md file. 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.py is optional. If present, it must contain an execute(args) function.
  • references/ is optional. Use it for detailed reference data (dimension tables, material properties) to keep SKILL.md concise.

Writing SKILL.md

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.

YAML Frontmatter (Optional)

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.

Progressive Disclosure

Skills use a layered loading system to manage context size:

  1. Name + description — always visible in the skills list (~10 words)
  2. SKILL.md body — loaded when the skill is invoked (keep under 200 lines)
  3. 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`.

Structure

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 dimensions

Tips for Good Skills

Be 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 height

Include 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=H to cut from the top face (otherwise no floor)
  • Boolean operations fail on coplanar faces
  • Revolution crashes if the profile crosses the axis
  • Always pass explicit length to pad_sketch (do not rely on the 10mm default)
  • Always pass body_name when 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.

Example SKILL.md

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.

Writing handler.py (Optional)

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.

When to Use a Handler

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

The execute() Function

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 Values

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.

Example handler.py

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}

Handler Loading

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.

Using /skill-creator

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:

  1. Capture intent — understand what the user wants, extract what it can from context
  2. Interview for details — ask about parameters, edge cases, construction approach, standard dimensions, and FreeCAD pitfalls (skipping questions already answered)
  3. Choose a name — pick a short, hyphenated name and confirm with the user
  4. Write the skill — generate SKILL.md (with YAML frontmatter) and optionally handler.py, using progressive disclosure (references/ for large data)
  5. Save the files — create the skill directory using execute_code
  6. 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.

Geometry Validation (VALIDATION.md)

Skills can optionally include a VALIDATION.md file that defines expected geometry properties. This enables two features:

  1. --validate flag -- users can append --validate to any skill invocation to check the result
  2. Optimizer scoring -- the /optimize-skill command uses validation rules to measure geometry correctness

File Structure

my-skill/
  SKILL.md
  VALIDATION.md      # Optional: geometry validation rules
  handler.py         # Optional: Python handler

Format

# 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

Parameter Types

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

Available Checks

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

Conditional Rules

Use #### when param == "value" to add checks that only apply for specific parameter values. The when block is scoped to its parent ### ObjectName section.

Expression Language

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.

What you can use

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

Examples

# 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

What you CANNOT use

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 (use when blocks for conditional logic)
  • Attribute access -- no obj.attr, no module.function()
  • Imports -- no import, no __import__
  • List/dict operations -- no [], no {}
  • Any Python built-in besides sqrt, abs, min, max -- no len(), 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.

Tolerances

  • Absolute: (tolerance 0.5) -- within 0.5mm
  • Relative: (tolerance 5%) -- within 5% of expected value

report_skill_params

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.

Using --validate

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

Testing Skills

After creating a skill, verify it works:

  1. Invoke the skill by typing the /command in the chat input.
  2. Check that the LLM follows the instructions -- watch the tool calls in the chat output. Verify the operations match your SKILL.md steps.
  3. Inspect the result -- rotate the 3D view, check dimensions, verify all features are present.
  4. Test with different parameters -- try edge cases (very small dimensions, zero fillet radius, maximum values).
  5. 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

Sharing Skills

Skills are fully portable. To share a skill:

  1. Copy the skill directory (e.g., <FreeCADAI dir>/skills/my-skill/) to a zip file, git repo, or file share.
  2. The recipient copies the directory into their own <FreeCADAI dir>/skills/.
  3. 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-skill

Next: Skills | Skills Reference | Tool Reference

Clone this wiki locally