Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/KiCad/kicad-source-mirror/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Action plugins allow you to extend KiCad’s PCB editor with custom interactive tools. They appear as menu items and optional toolbar buttons, providing a way to automate repetitive tasks or add new functionality.

Plugin Architecture

Action plugins are implemented through the ACTION_PLUGIN base class defined in /pcbnew/action_plugin.h:
class ACTION_PLUGIN
{
public:
    virtual ~ACTION_PLUGIN();
    
    // Required methods to implement
    virtual wxString GetCategoryName() = 0;
    virtual wxString GetName() = 0;
    virtual wxString GetClassName() = 0;
    virtual wxString GetDescription() = 0;
    virtual bool GetShowToolbarButton() = 0;
    virtual wxString GetIconFileName( bool aDark ) = 0;
    virtual wxString GetPluginPath() = 0;
    virtual void* GetObject() = 0;
    virtual void Run() = 0;
    
    // Registration
    void register_action();
    
    // UI elements
    int m_actionMenuId;
    int m_actionButtonId;
    wxBitmap iconBitmap;
    bool show_on_toolbar;
};

Creating an Action Plugin

Basic Structure

Create a Python file that inherits from ActionPlugin:
import pcbnew
import os

class MyActionPlugin(pcbnew.ActionPlugin):
    def defaults(self):
        """Define plugin metadata"""
        self.name = "My Custom Tool"
        self.category = "Modify PCB"
        self.description = "Does something useful with the PCB"
        self.show_toolbar_button = True
        self.icon_file_name = os.path.join(
            os.path.dirname(__file__), 'icon.png'
        )
        self.dark_icon_file_name = os.path.join(
            os.path.dirname(__file__), 'icon_dark.png'
        )
    
    def Run(self):
        """Execute when the plugin is invoked"""
        board = pcbnew.GetBoard()
        # Your plugin logic here
        pcbnew.Refresh()  # Update display

# Register the plugin
MyActionPlugin().register()

Plugin Metadata

The defaults() method sets plugin properties:
  • name: Display name in menu (required)
  • category: Submenu grouping (required)
  • description: Tooltip and help text (required)
  • show_toolbar_button: Add to toolbar (default: False)
  • icon_file_name: Path to icon (24x24 PNG recommended)
  • dark_icon_file_name: Icon for dark theme (optional)

Real-World Examples

Example 1: Automatic Border Creator

Based on /demos/python_scripts_examples/action_menu_add_automatic_border.py:
from pcbnew import *

class AddAutomaticBorder(ActionPlugin):
    """
    Automatically creates or updates PCB edges to include all elements
    """
    
    def defaults(self):
        self.name = "Add or update automatic PCB edges"
        self.category = "Modify PCB"
        self.description = "Automatically add or update edges on an existing PCB"
        # Offset between elements and edge (2.54mm)
        self.offset = FromMM(2.54)
        # Snap to grid (2.54mm)
        self.grid = FromMM(2.54)
    
    def min(self, a, b):
        """Helper to find minimum, handling None values"""
        if a is None:
            return b
        if b is None:
            return a
        return a if a < b else b
    
    def max(self, a, b):
        """Helper to find maximum, handling None values"""
        if a is None:
            return b
        if b is None:
            return a
        return a if a > b else b
    
    def Run(self):
        pcb = GetBoard()
        min_x = min_y = max_x = max_y = None
        
        # Find bounding box of all zones
        for i in range(pcb.GetAreaCount()):
            bbox = pcb.GetArea(i).GetBoundingBox()
            min_x = self.min(min_x, bbox.GetX())
            min_y = self.min(min_y, bbox.GetY())
            max_x = self.max(max_x, bbox.GetX() + bbox.GetWidth())
            max_y = self.max(max_y, bbox.GetY() + bbox.GetHeight())
        
        # Include all tracks
        for track in pcb.GetTracks():
            min_x = self.min(min_x, track.GetStart().x)
            min_y = self.min(min_y, track.GetStart().y)
            max_x = self.max(max_x, track.GetEnd().x)
            max_y = self.max(max_y, track.GetEnd().y)
        
        # Include all footprints
        for footprint in pcb.GetFootprints():
            bbox = footprint.GetBoundingBox()
            min_x = self.min(min_x, bbox.GetX())
            min_y = self.min(min_y, bbox.GetY())
            max_x = self.max(max_x, bbox.GetX() + bbox.GetWidth())
            max_y = self.max(max_y, bbox.GetY() + bbox.GetHeight())
        
        # Add offset
        min_x -= self.offset
        min_y -= self.offset
        max_x += self.offset
        max_y += self.offset
        
        # Snap to grid
        min_x = min_x - (min_x % self.grid)
        min_y = min_y - (min_y % self.grid)
        if (max_x % self.grid) != 0:
            max_x = max_x - (max_x % self.grid) + self.grid
        if (max_y % self.grid) != 0:
            max_y = max_y - (max_y % self.grid) + self.grid
        
        # Create edge shapes
        self._create_edge(pcb, min_x, min_y, min_x, max_y)  # West
        self._create_edge(pcb, min_x, min_y, max_x, min_y)  # North
        self._create_edge(pcb, max_x, min_y, max_x, max_y)  # East
        self._create_edge(pcb, min_x, max_y, max_x, max_y)  # South
    
    def _create_edge(self, pcb, x1, y1, x2, y2):
        """Create or update an edge line"""
        edge = PCB_SHAPE()
        edge.SetLayer(Edge_Cuts)
        edge.SetStart(wxPoint(x1, y1))
        edge.SetEnd(wxPoint(x2, y2))
        pcb.Add(edge)

AddAutomaticBorder().register()

Example 2: Footprint Generator

Based on /demos/python_scripts_examples/action_plugin_test_undoredo.py:
import pcbnew
import random

class GenerateRandomContent(pcbnew.ActionPlugin):
    
    def defaults(self):
        self.name = "Generate Random Test Content"
        self.category = "Test Tools"
        self.description = "Generate random footprints and tracks for testing"
    
    def Run(self):
        self.pcb = pcbnew.GetBoard()
        random.seed()
        
        for i in range(10):
            # Create random track segments
            seg = pcbnew.PCB_SHAPE()
            seg.SetLayer(random.choice([
                pcbnew.Edge_Cuts,
                pcbnew.Cmts_User,
                pcbnew.Eco1_User
            ]))
            seg.SetStart(pcbnew.VECTOR2I_MM(
                random.randint(10, 100),
                random.randint(10, 100)
            ))
            seg.SetEnd(pcbnew.VECTOR2I_MM(
                random.randint(10, 100),
                random.randint(10, 100)
            ))
            self.pcb.Add(seg)
            
            # Create random tracks and vias
            if i % 2 == 0:
                t = pcbnew.PCB_TRACK(None)
            else:
                t = pcbnew.PCB_VIA(None)
                t.SetViaType(pcbnew.VIATYPE_THROUGH)
                t.SetDrill(pcbnew.FromMM(random.randint(1, 20) / 10.0))
            
            t.SetStart(pcbnew.VECTOR2I_MM(
                random.randint(100, 150),
                random.randint(100, 150)
            ))
            t.SetEnd(pcbnew.VECTOR2I_MM(
                random.randint(100, 150),
                random.randint(100, 150)
            ))
            t.SetWidth(pcbnew.FromMM(random.randint(1, 15) / 10.0))
            t.SetLayer(random.choice([pcbnew.F_Cu, pcbnew.B_Cu]))
            self.pcb.Add(t)
            
            # Create random connector footprints
            self.create_fpc_footprint(random.randint(2, 40))
    
    def create_fpc_footprint(self, pads):
        """Create an FPC connector footprint"""
        footprint = pcbnew.FOOTPRINT(self.pcb)
        footprint.SetReference(f"FPC{pads}")
        footprint.Reference().SetPosition(pcbnew.VECTOR2I_MM(-1, -1))
        self.pcb.Add(footprint)
        
        # Create signal pads
        pad_size = pcbnew.VECTOR2I_MM(0.25, 1.6)
        for n in range(pads):
            pad = pcbnew.PAD(footprint)
            pad.SetSize(pad_size)
            pad.SetShape(pcbnew.PAD_SHAPE_RECT)
            pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD)
            pad.SetLayerSet(pad.SMDMask())
            pad.SetPosition(pcbnew.VECTOR2I_MM(0.5 * n, 0))
            pad.SetPadName(str(n + 1))
            footprint.Add(pad)
        
        # Create mounting pads
        mount_size = pcbnew.VECTOR2I_MM(1.50, 2.0)
        for i, x_offset in enumerate([-1.6, (pads-1)*0.5 + 1.6]):
            pad = pcbnew.PAD(footprint)
            pad.SetSize(mount_size)
            pad.SetShape(pcbnew.PAD_SHAPE_RECT)
            pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD)
            pad.SetLayerSet(pad.SMDMask())
            pad.SetPosition(pcbnew.VECTOR2I_MM(x_offset, 1.3))
            pad.SetPadName("0")
            footprint.Add(pad)
        
        # Add silkscreen
        silk = pcbnew.PCB_SHAPE(footprint)
        silk.SetStart(pcbnew.VECTOR2I_MM(-1, 0))
        silk.SetEnd(pcbnew.VECTOR2I_MM(0, 0))
        silk.SetWidth(pcbnew.FromMM(0.2))
        silk.SetLayer(pcbnew.F_SilkS)
        silk.SetShape(pcbnew.S_SEGMENT)
        footprint.Add(silk)
        
        # Random placement
        footprint.SetPosition(pcbnew.VECTOR2I_MM(
            random.randint(20, 200),
            random.randint(20, 150)
        ))

GenerateRandomContent().register()

Example 3: Move Items Randomly

import pcbnew
import random

class MoveItemsRandomly(pcbnew.ActionPlugin):
    
    def defaults(self):
        self.name = "Move Elements Randomly"
        self.category = "Test Undo/Redo"
        self.description = "Move all board elements by random offsets"
    
    def Run(self):
        pcb = pcbnew.GetBoard()
        
        # Move zones
        for i in range(pcb.GetAreaCount()):
            area = pcb.GetArea(i)
            area.Move(pcbnew.VECTOR2I_MM(
                random.randint(-20, 20),
                random.randint(-20, 20)
            ))
        
        # Move and randomly flip footprints
        for footprint in pcb.GetFootprints():
            footprint.Move(pcbnew.VECTOR2I_MM(
                random.randint(-20, 20),
                random.randint(-20, 20)
            ))
            if random.randint(0, 10) > 5:
                footprint.Flip(footprint.GetPosition(), True)
        
        # Move tracks
        for track in pcb.GetTracks():
            track.Move(pcbnew.VECTOR2I_MM(
                random.randint(-20, 20),
                random.randint(-20, 20)
            ))
        
        # Move drawings
        for draw in pcb.GetDrawings():
            draw.Move(pcbnew.VECTOR2I_MM(
                random.randint(-20, 20),
                random.randint(-20, 20)
            ))

MoveItemsRandomly().register()

Plugin Management

The ACTION_PLUGINS class manages all registered plugins:
class ACTION_PLUGINS
{
public:
    static void register_action( ACTION_PLUGIN* aAction );
    static bool deregister_object( void* aObject );
    static ACTION_PLUGIN* GetAction( const wxString& aName );
    static ACTION_PLUGIN* GetAction( int aIndex );
    static ACTION_PLUGIN* GetActionByMenu( int aMenu );
    static ACTION_PLUGIN* GetActionByButton( int aButton );
    static ACTION_PLUGIN* GetActionByPath( const wxString& aPath );
    static int GetActionsCount();
    static bool IsActionRunning();
    static void SetActionRunning( bool aRunning );
    static void UnloadAll();
};

Plugin Installation

Directory Structure

Plugins are loaded from these directories (in order):
  1. Stock plugins: <kicad>/scripting/plugins/
  2. User plugins: ~/.kicad/scripting/plugins/ (Linux/macOS)
  3. User plugins: %APPDATA%/kicad/scripting/plugins/ (Windows)
  4. Third-party: $KICAD_3RD_PARTY/plugins/

File Organization

plugins/
├── my_plugin.py          # Simple single-file plugin
└── complex_plugin/       # Multi-file plugin
    ├── __init__.py       # Must contain plugin class
    ├── helpers.py
    ├── icon.png
    └── icon_dark.png

Loading Process

Plugins are automatically loaded on startup via LoadPlugins() in /scripting/kicadplugins.i:
LoadPlugins(
    bundlepath="/usr/share/kicad/scripting",
    userpath="~/.kicad/scripting",
    thirdpartypath="$KICAD_3RD_PARTY"
)

Undo/Redo Support

Action plugins automatically support undo/redo when using proper methods:
def Run(self):
    board = pcbnew.GetBoard()
    
    # Use RemoveNative() for undo support
    for footprint in board.GetFootprints():
        board.RemoveNative(footprint)
    
    # Changes are automatically tracked
    # User can undo/redo after plugin execution

Icon Guidelines

  • Size: 24x24 pixels (PNG format)
  • Style: Monochrome or simple colors
  • Transparency: Use alpha channel for toolbar integration
  • Dark theme: Provide dark_icon_file_name for dark mode

Debugging

Error Handling

import traceback

class MyPlugin(pcbnew.ActionPlugin):
    def Run(self):
        try:
            # Your code here
            board = pcbnew.GetBoard()
        except Exception as e:
            # Errors are shown in KiCad's error dialog
            import wx
            wx.MessageBox(
                f"Error: {str(e)}\n\n{traceback.format_exc()}",
                "Plugin Error",
                wx.OK | wx.ICON_ERROR
            )

Logging

Avoid using print() as it can cause IO exceptions. Use wx dialogs or write to files instead.

Best Practices

  1. Always call Refresh(): Update the display after modifications
  2. Use GetBoard(): Don’t cache the board object
  3. Handle errors gracefully: Wrap code in try/except blocks
  4. Validate inputs: Check that board elements exist before accessing
  5. Use proper units: Always convert with FromMM() or FromMils()
  6. Test undo/redo: Ensure your plugin works with the undo system
  7. Avoid blocking operations: Long operations should show progress

Common Issues

Plugin Not Loading

Check PLUGIN_DIRECTORIES_SEARCH and NOT_LOADED_WIZARDS:
import pcbnew
print(pcbnew.GetWizardsSearchPaths())
print(pcbnew.GetUnLoadableWizards())
Ensure you:
  • Called .register() at module level
  • Implemented all required methods
  • Returned valid strings from metadata methods

See Also

Build docs developers (and LLMs) love