← How-Tos
3d-printing Aug 14, 2026 ◑ 3 views ◯ 5 min read

Klipper Macros: Writing Custom G-Code Macros, Conditionals, and Variables

klippergcode macromacrosjinja2conditionalsvariablesprinter.cfgstart printmoonrakercustom gcode

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 Ready

That 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 F300

This 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

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 tool: G-code Analyzer