Skip to content

Example: an agent watching a 3D print

Someone starts a long print and asks an agent to keep an eye on the print overnight. Through the OpenHAL hub the agent reads the printer's webcam, warms the bed, sends a request for a nozzle temperature of 300 °C that the hub blocks, pauses the print and sends a request to cancel the print, which the hub holds until the owner approves the cancel.

Every value on this page comes from docs/demo/scenario.yaml, which uv run openhal demo records when the hub runs the print babysitter example against a simulated printer and webcam. The quickstart runs the same demo in your terminal.

Step by step

Next draws each step's request along the lifelines, with the hub's reply as a dashed arrow back to the sender.

  1. You start a long print and ask an agent to watch it.

    watch my print

  2. The agent checks the webcam and sees a corner of the first layer lifting.

    read frame done

  3. The agent sends a request to set the bed to 70 °C. Your limit is 110 °C, so the hub passes the request to the printer.

    bed_target 70 °C done

  4. The agent also sends a request for a nozzle temperature of 300 °C to help the print stick. Your limit is 260 °C, so the hub blocks the request.

    extruder_target 300 °C LIMIT_EXCEEDED

    extruder_target 300 °C exceeds the limit of 260 °C

  5. Later, the webcam shows the print has come loose.

    read frame done

  6. The agent pauses the print. Pausing is safe, so no one has to approve it.

    pause done

  7. Cancelling can't be undone, so the hub holds the request until you approve it.

    cancel pending

  8. You approve, and the hub sends the cancel to the printer.

    approve cancel done

  9. The audit log records all 6 requests, including the blocked request and your approval.

The device file

The hub checks every request against printer.hal.yaml, the printer's device file, which the owner confirmed on 2026-10-24. The file limits the nozzle target to 260 °C and the bed target to 110 °C, and the cancel action needs a person's approval each time.

openhal: "0.1"
device: Klipper 3D printer
id: printer
match:
  mdns: _moonraker._tcp
  moonraker: { hostname: "printer*" }
connect: { moonraker: printer.local, port: 7125, timeout: 5 s }
read:
  state: { type: enum, values: [standby, printing, paused, complete, cancelled, error], every: 1 s }
  extruder_temp: { unit: degC, every: 1 s, range: [0, 320] }
  bed_temp: { unit: degC, every: 1 s, range: [0, 130] }
  progress: { unit: percent, every: 5 s }
write:
  extruder_target: { unit: degC, min: 0, max: 260, max_change_per_s: 50 }
  bed_target: { unit: degC, min: 0, max: 110, max_change_per_s: 20 }
actions:
  pause:
    description: Pause the print. Klipper parks the nozzle and keeps the heaters on.
    require: ["state == printing"]
  resume:
    description: Resume a paused print.
    require: ["state == paused"]
  cancel:
    description: Cancel the print. It can't be resumed afterwards.
    confirm: confirm-each-time
safe_state:
  - { extruder_target: 0, bed_target: 0 }
interlocks:
  - if: "extruder_target > 0 degC"
    require: "state != error"
    message: "Clear the printer error before heating."
watchdog: { heartbeat: 5 s, on_loss: hold }
hazard: medium
notes: "A print keeps running if the hub goes away, so the watchdog holds the printer's state instead of stopping the print. Klipper's own heater checks stay on either way."
provenance: { source: hand-written, by: openhal print babysitter example, at: 2026-10-24 }
confirmed: { by: owner, at: 2026-10-24, sha256: 43396b8f02a70481b9653275f22cc58f96b060ad2e31f4d80214acb26e51af1a }

The messages

Each request is the method and params the agent or the owner sent, and each response is the hub's result or error, as the record holds them. The record leaves out each message's jsonrpc and id members, keeps a camera frame's size in place of its image data, and writes REQUEST_ID for the id the hub gave the held cancel.

  1. 1 The agent checks the first layer. agent:claude

    Request

    {
      "method": "hal/read",
      "params": { "device": "printer-cam", "channel": "frame" }
    }

    Response

    {
      "result": {
        "device": "printer-cam",
        "channel": "frame",
        "value": {
          "mimeType": "image/jpeg",
          "bytes": 19409,
          "width": 640,
          "height": 480
        },
        "unit": null,
        "suspect": false
      }
    }
  2. 2 A corner of the first layer is lifting. agent:claude

    Request

    {
      "method": "hal/write",
      "params": {
        "device": "printer",
        "channel": "bed_target",
        "value": 70,
        "unit": "degC"
      }
    }

    Response

    {
      "result": {
        "status": "done",
        "device": "printer",
        "channel": "bed_target",
        "value": 70,
        "unit": "degC"
      }
    }
  3. 3 A hotter nozzle might help the corner stick. agent:claude

    Request

    {
      "method": "hal/write",
      "params": {
        "device": "printer",
        "channel": "extruder_target",
        "value": 300,
        "unit": "degC"
      }
    }

    Error response

    {
      "error": {
        "code": -32010,
        "message": "extruder_target 300 degC exceeds the limit of 260 degC (printer.hal.yaml, confirmed by owner 2026-10-24)",
        "data": {
          "name": "LIMIT_EXCEEDED",
          "kernelCode": "above_max",
          "kernelMessage": "Blocked. The extruder_target limit is 260 degC (max). Requested 300 degC.",
          "device": "printer",
          "channel": "extruder_target",
          "requested": { "value": 300, "unit": "degC" },
          "limit": { "value": 260, "unit": "degC" },
          "file": "printer.hal.yaml",
          "confirmedBy": "owner",
          "confirmedAt": "2026-10-24"
        }
      }
    }
  4. 4 Later the agent checks the print again. agent:claude

    Request

    {
      "method": "hal/read",
      "params": { "device": "printer-cam", "channel": "frame" }
    }

    Response

    {
      "result": {
        "device": "printer-cam",
        "channel": "frame",
        "value": {
          "mimeType": "image/jpeg",
          "bytes": 23567,
          "width": 640,
          "height": 480
        },
        "unit": null,
        "suspect": false
      }
    }
  5. 5 The print has come loose. agent:claude

    Request

    {
      "method": "hal/action",
      "params": { "device": "printer", "action": "pause" }
    }

    Response

    {
      "result": {
        "status": "done",
        "result": {
          "device": "printer",
          "action": "pause",
          "args": {}
        }
      }
    }
  6. 6 Cancelling can't be undone. agent:claude

    Request

    {
      "method": "hal/action",
      "params": { "device": "printer", "action": "cancel" }
    }

    Response

    {
      "result": {
        "status": "pending",
        "requestId": "REQUEST_ID",
        "expiresInS": 300
      }
    }
  7. 7 The owner approves the cancel. human:owner

    Request

    {
      "method": "hal/confirmations/approve",
      "params": { "requestId": "REQUEST_ID" }
    }

    Response

    {
      "result": {
        "status": "done",
        "device": "printer",
        "target": "action:cancel",
        "value": {},
        "unit": null
      }
    }

The audit log

The run ends with uv run openhal log -n 8, which prints the hub's audit log, newest first. The log holds every request, including the blocked one, and the owner's approval.

Seq Event Who Target Status Message
13 action agent:claude printer.cancel done Done. cancel ran.
12 confirm human:owner printer.cancel done
11 action agent:claude printer.cancel pending Held until a human approves action:cancel on printer.
10 action agent:claude printer.pause done Done. pause ran.
9 read agent:claude printer-cam.frame done
8 write agent:claude printer.extruder_target blocked Blocked. The extruder_target limit is 260 degC (max). Requested 300 degC.
7 write agent:claude printer.bed_target done Done. bed_target is now 70 degC.
6 read agent:claude printer-cam.frame done

6 requests, 1 blocked, 1 confirmed by a person.