Swapping between smooth PEI, textured PEI, G10 and coated steel moves the first layer, because each plate has its own thickness. Three small config blocks give each plate its own saved Z offset, applied every print.
Klipper has no built-in build plate profiles, so an automatic Z offset per plate takes three pieces: a [save_variables] file holding one offset per plate, a SET_PLATE macro that applies the selected plate’s offset with SET_GCODE_OFFSET, and a line in PRINT_START that applies it again before every print. Measure each plate once against a reference plate and store the difference.
Klipper’s bed leveling guide explains why: “different tapes (or any other printing surface) will impact Z measurements. Be sure to rerun the paper test to measure each type of surface that is in use.”
How do you set a different Z offset for each build plate in Klipper?
Store one offset per plate with Klipper’s [save_variables], then apply it with SET_GCODE_OFFSET Z=<value> whenever you select a plate. A small SET_PLATE PLATE=textured macro saves the plate name, looks up that plate’s offset and applies it. PRINT_START runs it again every print because a G-code offset resets when Klipper restarts.
[save_variables]
filename: ~/printer_data/config/variables.cfg
[gcode_macro SET_PLATE]
description: Select a build plate and apply its saved Z offset
gcode:
{% set svv = printer.save_variables.variables %}
{% set plate = params.PLATE|default(svv.plate|default("none"))|lower %}
{% set key = "offset_" ~ plate %}
{% if key not in svv %}
{action_respond_info("No saved offset for plate '%s', using 0" % plate)}
{% endif %}
{% set z = svv[key]|default(0.0)|float %}
SAVE_VARIABLE VARIABLE=plate VALUE='"{plate}"'
SET_GCODE_OFFSET Z={z}
{action_respond_info("Plate %s: Z offset %.3f" % (plate, z))}
[gcode_macro SAVE_PLATE_OFFSET]
description: Store the current Z offset for the selected plate
gcode:
{% set svv = printer.save_variables.variables %}
{% set plate = svv.plate|default("none") %}
{% set z = printer.gcode_move.homing_origin.z|round(3) %}
SAVE_VARIABLE VARIABLE=offset_{plate} VALUE={z}
{action_respond_info("Saved Z offset %.3f for plate %s" % (z, plate))}
What each part does:
[save_variables] needs only filename. Keeping the file in the config folder lets you open it in the web interface’s editor.
SET_PLATE takes the plate from PLATE= or the last one saved, and replaces the whole G-code Z offset with that plate’s offset_<plate>. Use short lowercase names (smooth, textured, g10, steel). The G-code reference says “The VARIABLE must be lowercase.”
VALUE='"{plate}"' has two sets of quotes because Klipper parses the value “as a Python literal”. A bare word fails with “Unable to parse … as a literal”.
SAVE_PLATE_OFFSET reads printer.gcode_move.homing_origin.z, the current G-code Z offset including any babystepping, and stores it for the selected plate.
SET_GCODE_OFFSET Z= sets an absolute offset. Positive values raise the nozzle.
How to measure the Z offset of each build plate
- Calibrate the reference plate. Fit the plate you use most. Run
PROBE_CALIBRATE if Z homes with a probe, or Z_ENDSTOP_CALIBRATE for a Z switch, then SAVE_CONFIG. Store its offset as zero, for example SAVE_VARIABLE VARIABLE=offset_smooth VALUE=0.
- Measure it cold. Klipper’s paper test needs nozzle and bed at room temperature. Run
G28, SET_GCODE_OFFSET Z=0 and BED_MESH_CLEAR. Move the nozzle to the bed center and run MANUAL_PROBE. Lower it with TESTZ Z=-.1 until a sheet of paper drags lightly, then ACCEPT. Note the “Z position is” value.
- Swap and re-home. Fit the next plate and run
G28 so homing sees it. Run BED_MESH_CLEAR and repeat MANUAL_PROBE at the same spot.
- Store the difference. The plate’s offset is its reading minus the reference reading. A thicker plate gives a positive number:
SAVE_VARIABLE VARIABLE=offset_g10 VALUE=0.15 (example value).
- Fine-tune on a first layer. Run
SET_PLATE PLATE=g10, print a one-layer test, babystep until the lines look right, then run SAVE_PLATE_OFFSET.
Mainsail’s Z offset buttons send SET_GCODE_OFFSET Z_ADJUST=, which “will be added to any existing offset”, so babystepping starts from the plate’s value. From the console, SET_GCODE_OFFSET Z_ADJUST=-0.02 MOVE=1 does the same.
Klipper macro for Z offset in PRINT_START
Call SET_PLATE near the end of your start macro, just before the purge line:
[gcode_macro PRINT_START]
gcode:
{% set svv = printer.save_variables.variables %}
{% set plate = params.PLATE|default(svv.plate|default("none")) %}
G28
# ... your heating, leveling and BED_MESH_CALIBRATE lines ...
SET_PLATE PLATE={plate}
# ... purge line, then the print starts
G28 does not clear the offset, because Klipper’s homing code restores it after each home. Only lines that reset the offset must come before SET_PLATE. If your slicer can send the plate type, add PLATE= to its PRINT_START call (see macro parameters). Otherwise run SET_PLATE PLATE=<name> once when you swap, and later prints reuse that plate.
The offset resets to zero on every restart. To apply the saved plate at startup too, add a delayed G-code:
[delayed_gcode APPLY_PLATE_AT_STARTUP]
initial_duration: 1
gcode:
SET_PLATE
For quick manual switching, Mainsail’s preset settings suggest custom G-code “for different print surfaces”. A preset whose G-code is SET_PLATE PLATE=textured gives you one button per plate.
Mainsail’s save button runs Z_OFFSET_APPLY_PROBE when Z homes with a probe, or Z_OFFSET_APPLY_ENDSTOP otherwise, then offers SAVE_CONFIG. Those commands fold the current offset into the probe’s z_offset or the position_endstop. That shifts the baseline for every plate, so each stored offset ends up wrong by the same amount. Use SAVE_PLATE_OFFSET for per-plate tuning. Press the save button only on the reference plate, when the whole machine’s baseline has moved, for example after a nozzle change.
Does the probe type change the offsets?
Yes. A Z endstop switch triggers off the frame, so a plate’s full thickness difference shows up in the first layer. A probe that touches the surface measures most of the thickness itself. An inductive probe senses the steel under the surface, so it misses the coating’s thickness. On a plate with no steel under the surface, check that it triggers before you home. The measuring steps above give the real number for your printer either way. A difference near zero means that plate needs no offset.
If the per-plate offset does not apply
- Offset is 0 during the print: a later line, often in the slicer’s start G-code or another macro, sends
SET_GCODE_OFFSET Z=0. Move SET_PLATE after it. Mainsail’s clear button next to the Z offset sends the same command.
- “No saved offset for plate”: the name does not match a stored
offset_<name>. Open variables.cfg and check the spelling.
- First layer still off after a swap: re-home with the new plate fitted. The offset is relative to wherever Z homed.