Klipper Macros: Writing Custom G-Code Macros, Conditionals, and Variables
Our Klipper installation, input shaper, and bed mesh guides get a printer running well on Klipper's core features. This one covers the feature that makes Klipper feel like a genuinely programmable printer rather than just a faster Marlin: [gcode_macro] blocks, which let you define custom G-code commands that run real logic — conditionals, variables, loops, and math — instead of just replaying a fixed sequence of moves. If you've ever wanted your START_PRINT to behave differently for a first layer on a cold morning versus a warm afternoon, or wanted a single PURGE_LINE command that adapts to whatever nozzle temperature is already set, macros are how Klipper does that.
The Basic Macro Structure
A macro lives in your printer.cfg (or an included config file) as a named block, and once defined it's callable from your slicer's start/end G-code, from the Klipper console, or from another macro:
[gcode_macro START_PRINT] gcode: G28 G1 Z10 F600 M117 ReadyThat alone is just a named sequence — the equivalent of a Marlin custom start script. What makes Klipper macros different is that everything inside the gcode: block is actually processed by Klipper's built-in Jinja2 templating engine before it's sent to the printer, meaning you can use real conditionals and variables anywhere in that block, not just a fixed list of commands.
Parameters: Making Macros Accept Slicer Values
Slicers pass values like target bed and nozzle temperature into your start macro as parameters, and Klipper macros read them with params.NAME, with a default fallback if the slicer doesn't provide one:
[gcode_macro START_PRINT] gcode: {% set bed_temp = params.BED_TEMP|default(60)|float %} {% set extruder_temp = params.EXTRUDER_TEMP|default(200)|float %} M140 S{bed_temp} M104 S{extruder_temp} M190 S{bed_temp} M109 S{extruder_temp}Your slicer's start G-code then calls START_PRINT BED_TEMP=[first_layer_bed_temperature] EXTRUDER_TEMP=[first_layer_temperature], and the macro picks up whatever values that specific slice actually used — so one macro correctly handles every filament profile you slice with, instead of needing per-material start scripts.
Conditionals: The Feature That Changes Everything
This is the part with no real Marlin equivalent. A Klipper macro can branch based on printer state, sensor readings, or passed parameters:
[gcode_macro CONDITIONAL_PURGE] gcode: {% if printer.extruder.temperature < 190 %} M109 S200 {% else %} M109 S{printer.extruder.target} {% endif %} G1 E20 F300This reads the printer's live extruder temperature through Klipper's printer object (which exposes nearly every piece of live printer state — toolhead position, heater status, fan speed, even bed mesh data) and only forces a full heat-up if the nozzle is genuinely cold, skipping the wait on a warm second print in the same session. That kind of "only do this if it's actually needed" logic is the single biggest quality-of-life improvement macros bring over a fixed start script.
Variables That Persist Across Macro Calls
A macro can define persistent state with variable_ config entries, which survive between calls (though not across a Klipper restart unless saved explicitly) — useful for things like tracking whether a bed mesh has already been run this session, or counting how many layers have printed for a custom pause-every-N-layers behavior:
[gcode_macro TRACK_PURGE] variable_purge_count: 0 gcode: SET_GCODE_VARIABLE MACRO=TRACK_PURGE VARIABLE=purge_count VALUE={printer['gcode_macro TRACK_PURGE'].purge_count + 1} {% if printer['gcode_macro TRACK_PURGE'].purge_count > 5 %} M117 Time to clean the nozzle {% endif %}A Realistic START_PRINT: Putting It Together
A well-built START_PRINT macro typically combines several of these pieces: parameters for temps, a conditional bed mesh call that skips re-meshing if one was already run recently, a conditional purge line whose length depends on nozzle diameter, and a final positioning sequence — all as one command your slicer calls with a handful of parameters, replacing what would otherwise be a long, rigid custom G-code block duplicated (and drifted out of sync) across every slicer profile you own.
Common Pitfalls
- Whitespace and indentation sensitivity. Jinja2 templating in a Klipper config is picky about indentation inside {% if %} blocks — a misaligned {% else %} is one of the most common first-macro errors, and Klipper's error message for it isn't always obvious about which line is actually wrong.
- Referencing printer state that doesn't exist yet. Calling printer.heater_bed.temperature before the bed heater config section has loaded, or referencing a sensor object name that doesn't match your actual config section name, throws a macro error at the exact moment you don't want one — mid-print-start.
- Forgetting macros run every time they're called, in full. A macro isn't a one-time setup script — if your END_PRINT macro includes homing or bed-mesh calls, they'll rerun every single time END_PRINT is called, which is rarely what you want for something that should only happen once per session.
- Overwriting a built-in G-code command without calling the original. Redefining G28 as a macro (common for adding pre-homing checks) needs to explicitly call the renamed original homing command inside the macro, or your printer silently stops actually homing.
Testing Macros Safely
Test new or edited macros from the Mainsail/Fluidd console with the printer idle and nozzle away from the bed before trusting them in an actual print's start G-code — a conditional bug in a macro that runs before the first layer can drive the toolhead somewhere unexpected, and it's much cheaper to catch that on the console than mid-print. Klipper's RESTART command reloads config changes (including macro edits) without a full reboot, which makes the edit-test-edit loop for macro development fast once you're used to it.
Macros are the feature that turns Klipper from "Marlin but faster" into something closer to a real scripting environment for your printer, and once you've written a couple of conditional macros the fixed, copy-pasted start G-code blocks common in Marlin-based setups start to feel obviously limiting by comparison. Start small — a parameterized START_PRINT with one conditional is enough to feel the difference — before reaching for the more advanced persistent-variable patterns.
Related Guides
- How to Install Klipper on Any 3D Printer: Complete Setup Guide
- How to Manage Klipper Firmware on the Anycubic Kobra 3 V2 with ACE Pro
- Running a Small 3D Print Farm: Fleet Management, Job Queuing, and Spool Inventory with Spoolman
- How to Calibrate E-Steps and Flow Rate for Dimensional Accuracy
- How to Calibrate Input Shaper and Pressure Advance in Klipper
- OrcaSlicer Complete Guide: Every Setting Explained for Maximum Print Quality