n8n workflow that cuts power to the X1C’s chamber heater (an Inkbird smart switch, exposed in Home Assistant as switch.inkbird) once a print ends. It subscribes to the printer’s Bambu MQTT report topic and, on the RUNNING → FINISH/FAILED transition, turns the Inkbird off if it’s on. The Inkbird is turned on manually via Siri/Alexa — either at the start of a print or to preheat the chamber before one. This workflow handles only the auto-off side. As of 2026-06-06 the design is idempotent and preheat-safe — it acts only on real end-of-print transitions, not on steady-state FINISH telemetry, so manually turning the Inkbird on while the printer sits idle no longer gets cancelled out.
Quick Reference
| Item | Value |
|---|---|
| n8n workflow ID | MB5ajMgX9zg46yFP |
| Workflow name | 3DWorkFlow |
| Active version | 6c17b9ab-462b-4420-89ff-8e9f5f82fae4 (2026-06-06) |
| Trigger type | MQTT — device/00M09D522600299/report (Bambu printer report topic) |
| Printer | Bambu Lab X1C — serial 00M09D522600299 |
| HA entity controlled | switch.inkbird |
| Error workflow | TX2y7dUINVnLM3ze — “n8n Error Notification” |
| Status | ACTIVE |
Architecture
Single execution path, 6 nodes, fully idempotent. Every MQTT message walks the chain; only the RUNNING → FINISH/FAILED transition gets past Print Finished?, and the Inkbird On? guard means the turn-off fires at most once per finish.
MQTT Trigger (subscribes to device/.../report — every status update fires this)
↓
Track State (Code: read/write previousGcodeState in workflow staticData;
emit isFinishTransition = previous==RUNNING && current ∈ {FINISH,FAILED})
↓
Print Finished? (IF: isFinishTransition == true)
↓ [true]
Get Inkbird State (Home Assistant: read switch.inkbird state)
↓
Inkbird On? (IF: state == "on")
↓ [true]
Turn Inkbird Off (Home Assistant: switch.turn_off)
| Node | Type | Key config |
|---|---|---|
| MQTT Trigger | mqttTrigger | topic device/00M09D522600299/report; jsonParseBody: true, onlyMessage: false, parallelProcessing: false; cred “MQTT account” |
| Track State | code v2 | JS reads $getWorkflowStaticData('global').previousGcodeState, compares with $json.message.print.gcode_state, writes current back. Emits { previousState, currentState, isFinishTransition }. |
| Print Finished? | if v2.2 | {{ $json.isFinishTransition }} is true (boolean) |
| Get Inkbird State | homeAssistant | resource state, entity switch.inkbird; cred “Home Assistant account”; onError: continueRegularOutput |
| Inkbird On? | if v2.2 | {{ $json.state }} equals on |
| Turn Inkbird Off | homeAssistant | service call, domain switch, service turn_off, entity_id switch.inkbird; onError: continueRegularOutput |
Track State JS body:
const staticData = $getWorkflowStaticData('global');
const previousState = staticData.previousGcodeState ?? null;
const currentState = $input.first().json.message?.print?.gcode_state ?? null;
const isFinishTransition = previousState === 'RUNNING' && (currentState === 'FINISH' || currentState === 'FAILED');
staticData.previousGcodeState = currentState;
return [{ json: { previousState, currentState, isFinishTransition } }];How it works
The Bambu X1C publishes its full status to device/<serial>/report over MQTT roughly once a second. The MQTT Trigger fires on every message; Track State records the new gcode_state and compares with the prior one held in workflow staticData. Print Finished? passes only when the just-observed transition was RUNNING → FINISH or RUNNING → FAILED. On a pass, Get Inkbird State reads switch.inkbird from Home Assistant, and if it’s on, Turn Inkbird Off calls switch.turn_off.
The edge-trigger is what makes the workflow preheat-safe. The printer keeps republishing FINISH until the next print starts (could be hours, could be days), so a pure level-trigger on gcode_state == FINISH would cancel any manual Inkbird-on the moment it landed. With the transition check, FINISH → FINISH ticks fall through (previous already equals current) and the manual on stands; only the one RUNNING → FINISH tick at end-of-print actually fires the turn-off.
The Inkbird On? guard is kept as a second-line defence: even if Track State somehow misfires (e.g. n8n restart loses staticData, see corner cases below), turning off an already-off switch is a harmless no-op. Both HA nodes are set to continue-on-error, so a transient HA blip won’t strand the heater on the one tick that mattered — but note the trade-off described under Known limitations.
What Track State does and doesn’t persist
$getWorkflowStaticData('global') is per-workflow JSON that n8n flushes to the DB after each successful execution. Reads + writes are scoped to this workflow, so there’s no cross-workflow contention. The static-data write only persists if the execution itself reaches a node-output write — which every tick does (the IF nodes run synchronously and never Wait), so the failure mode that bit us 2026-06-02 (suspended execution dying mid-Wait) doesn’t exist here.
On first execution after a deploy, previousGcodeState is null. The transition check is previous === 'RUNNING', so the first tick can never fire — even if the printer is mid-RUNNING → FINISH at that exact instant. Worst case after a deploy during the actual end-of-print second: one missed auto-off. Acceptable; the next print’s end will be caught normally.
State machine in a sentence
Fires exactly when previousGcodeState == RUNNING and currentGcodeState ∈ {FINISH, FAILED}. Everything else passes through without touching the Inkbird.
State Detection
Bambu’s gcode_state values are IDLE / PREPARE / RUNNING / PAUSE / FINISH / FAILED. The workflow only acts on the RUNNING → FINISH and RUNNING → FAILED transitions; everything else (including the steady FINISH → FINISH while the printer sits idle) is ignored.
Why require previous === RUNNING and not just any transition into FINISH/FAILED?
PREPARE → FAILEDhappens when a print bails out before it ever extrudes (load failure, bed-level fail). The heater wasn’t running for an actual print yet, and if you’d manually preheated, you’d want that preheat to stand. Narrow check passes; broad check would cancel the preheat. Worth the trade-off.PAUSE → FAILED(user cancels a paused print) is a rare corner where the narrow check leaves the heater on. Manually turn it off in that case. See Known limitations.
Failed-mid-print is RUNNING → FAILED, which fires — heater off, as intended.
Credentials Needed
MQTT account(mqtt) — Bambu MQTT broker credentials. Used byMQTT Trigger.Home Assistant account(homeAssistantApi) — HA long-lived access token. Used byGet Inkbird StateandTurn Inkbird Off.
2026-06-06 rewrite (edge-trigger for preheat support)
Symptom: turning the Inkbird on to preheat the chamber while the printer sat idle (post-print) cancelled itself within ~1 second.
Root cause: the 2026-06-02 idempotent design level-triggered on gcode_state == FINISH. Bambu republishes FINISH on every MQTT tick until the next print starts, so as soon as the manual Inkbird-on lifted state to on, the very next tick fired turn_off. The design was idempotent (the only correctness goal at the time) but not preheat-aware — the wiki itself documented “FINISH persists until the next print starts” without flagging the side effect.
Fix: edge-trigger. Inserted a Track State Code node between the MQTT trigger and the IF; it persists previousGcodeState via $getWorkflowStaticData('global') and emits isFinishTransition only when the just-observed transition was RUNNING → FINISH or RUNNING → FAILED. The IF now gates on that boolean instead of on the raw gcode_state value. Steady-state FINISH → FINISH ticks are now ignored; the Inkbird’s manual state is respected.
Why not just use the orphaned 3dPrint_v2 data table for previous-state tracking? Workflow staticData is lighter (no external table, no extra read), scoped per workflow, and has no separate failure mode. The data table is left as-is per the comment-don’t-delete convention.
2026-06-02 incident & first rewrite
Symptom: the chamber heater stayed on after every print.
Root cause: the original design used a boolean lock (data table 3dPrint_v2, row lock) to debounce repeated FINISH messages. The Release Lock node sat on the far side of a 10-minute Wait. n8n restarted (Watchtower auto-redeploy) while an execution was suspended mid-Wait; that execution died before Release Lock ran, leaving state = true permanently. From then on every FINISH dead-ended at the Lock Open? gate and the Inkbird was never touched. The lock row had been stuck at true since 2026-06-01 17:36 UTC.
Diagnosis trail: the MQTT trigger was firing ~1/sec and all executions were taking the false branch (normal). Reading the lock row showed state = true while nothing was printing → confirmed deadlock.
Fix: (1) manually reset the lock row to false to unblock immediately; (2) rewrote the workflow idempotent — dropped Get Lock Row, Lock Open?, Engage Lock, Wait, and Release Lock. Turning off an already-off switch is a harmless no-op, so the lock was solving a non-problem while introducing a deadlock class. Removing it eliminated the failure mode. (The level-triggered design that resulted lasted four days before the preheat use case surfaced — see 2026-06-06 above.)
Lesson: don’t gate a stateful lock behind a long Wait node in a stack that auto-redeploys. Prefer idempotency, or a timestamp-column debounce that holds no suspended execution.
Known limitations
- HA failures during the one transition tick are swallowed silently.
onError: continueRegularOutputon both HA nodes means a genuine HA failure won’t reach the error-notification workflow. Under the old level-triggered design this was cheap — the 1 Hz retry caught it on the next tick. Under the edge-triggered design, a transient HA failure on the singleRUNNING → FINISHtick will strand the heater on because the next tick isFINISH → FINISHand falls through. If this ever bites in practice, route theTurn Inkbird Offerror path toTX2y7dUINVnLM3zeso HA outages surface; for now, accepting the rare miss is cheaper than re-introducing a level-triggered retry that would conflict with preheat. - n8n restart at the exact end-of-print second drops that one auto-off. First tick after a deploy has
previousGcodeState == null; if that tick is theRUNNING → FINISHone, no fire. Vanishingly rare; manually turn off if you see it. PAUSE → FAILEDdoes not fire. User cancelling a paused print leaves the heater on. Narrowprevious == RUNNINGcheck trades this for preheat safety onPREPARE → FAILED. Manually turn off in that case.- Orphaned data table.
3dPrint_v2(GYmAXdbJ1uuQnCpq) is no longer referenced by the workflow but is left in place per the comment-don’t-delete convention. Harmless. gcode_statefield path assumption.Track Statereads$json.message.print.gcode_state. Bambu’s MQTT schema has shifted across firmware versions; ifisFinishTransitionstops going true after a firmware update, dump a raw MQTT message from the execution log and verify the path. Symptom: workflow stays Active but never fires the turn-off even after prints finish.
Disabling
n8n UI → workflow MB5ajMgX9zg46yFP → toggle Active off. The MQTT subscription drops; the printer can still publish, but nothing acts on it. The Inkbird stays in whatever state you last left it in. To disable only the turn-off (e.g. during HA maintenance), disable the Turn Inkbird Off node directly.
Related
- Error notification workflow: n8n workflow
TX2y7dUINVnLM3ze— “n8n Error Notification”. - Home Assistant entity:
switch.inkbird. Manual on via Siri/Alexa integration (configured outside n8n). - Printer MQTT: Bambu Lab X1C — see Bambu firmware MQTT docs for full payload schema.
- Orphaned data table:
3dPrint_v2(GYmAXdbJ1uuQnCpq) in projectXvuMFwjHIm5TIOoY— formerly held the lock row; now unused, left in place.
Changelog
- 2026-06-06 — Edge-trigger rewrite for preheat support. Inserted
Track StateCode node between MQTT Trigger and Print Finished?; persistspreviousGcodeStatevia$getWorkflowStaticData('global').Print Finished?now gates onisFinishTransition(previous == RUNNING && current ∈ {FINISH, FAILED}) instead of on rawgcode_state. Cancels the chain on steady-stateFINISH → FINISHticks so a manually-toggled Inkbird (for preheat) stays on. Narrowprevious == RUNNINGcheck chosen over broad “any transition into FINISH/FAILED” to preserve preheat survival acrossPREPARE → FAILEDprint-prep aborts; trade-off is thatPAUSE → FAILEDno longer auto-fires (see Known limitations). - 2026-06-02 — Rewrote idempotent. Dropped the lock entirely (
Get Lock Row,Lock Open?,Engage Lock,Wait,Release Lock) after a stuck-lock deadlock left the heater on after every print (n8n restarted mid-Wait,Release Locknever ran). New chain wasMQTT Trigger → Print Finished? → Get Inkbird State → Inkbird On? → Turn Inkbird Off. TheInkbird On?guard + harmless re-issue ofturn_offreplaced the lock’s debounce role with no suspended-execution failure mode. Data table3dPrint_v2orphaned but retained. (Superseded by the 2026-06-06 edge-trigger rewrite when preheat use case surfaced.) - 2026-05-21 — Full revamp (lock-based design, now superseded).
- Recreated the underlying data table as
3dPrint_v2(GYmAXdbJ1uuQnCpq) after the legacy3dPrint(398wc4bsCyFrdKOY) lost its backing Postgres relation. Legacy metadata entry left in place — harmless. - Switched trigger from
mc_remaining_time == 1togcode_state == FINISH OR FAILED. Catches real end-of-print and also catches failed prints (Inkbird still gets turned off, for safety). - Reordered: lock engaged before the Inkbird state check; added
Release Lock+ extended Wait to 10min; removed 5 dormant orphan nodes; HA nodes set toonError: continueRegularOutput; renamed all nodes for readability.
- Recreated the underlying data table as