Werkspur Docs Zur Website

Aktionen

Was ist eine Aktion

Eine Aktion ist ein benannter, serverseitig hinterlegter Ablauf aus Kommando-Schritten. Ein Widget einer Ansicht, das Terminal, ein Trigger oder der Assistent ruft sie über ihren Namen auf. Der Aufrufer kennt nur den Namen und die erwarteten Eingaben, nicht die Schritte dahinter.

Die Schritte laufen genau in der Reihenfolge ihrer Definition; ein externes Kommando bleibt dabei an seiner Position zwischen den anderen Kommandos. Alle Datenbankänderungen einer Aktion laufen in einer einzigen Transaktion. Scheitert ein Schritt, wird diese Transaktion zurückgerollt. Ein einzelnes Kommando bucht genau eine Sache; eine Aktion fasst mehrere Kommandos zu einem Vorgang zusammen, den die Datenbank als Ganzes annimmt oder verwirft.

Ein Fehler rollt zuerst die Datenbankänderungen zurück. Danach laufen die Gegen-Schritte bereits ausgeführter externer Aufrufe in umgekehrter Reihenfolge. Diese Kompensationen werden bestmöglich versucht; ihr Fehlschlag ändert nichts daran, dass die ursprüngliche Aktion fehlgeschlagen ist.

flowchart TD
    Call["Aktion aufrufen"] --> Begin["Transaktion beginnen"]
    Begin --> Next["Nächstes Kommando der Definition"]
    Next --> Run["Kommando ausführen, intern oder extern"]
    Run --> Success{"Kommando erfolgreich?"}
    Success -->|"Ja"| More{"Weitere Kommandos?"}
    More -->|"Ja"| Next
    More -->|"Nein"| Commit["Transaktion festschreiben"]
    Success -->|"Nein"| Rollback["Datenbank zurückrollen"]
    Rollback --> Compensate["Kompensationen rückwärts bestmöglich ausführen"]
    Compensate --> Error["Fehler zurückgeben"]

Eine Aktion deklariert typisierte Parameter. Jeder Parameter stammt entweder vom Bediener, der ihn eingibt oder scannt, oder aus dem Kontext. Die Station liefert Kontextparameter automatisch mit, zum Beispiel die aktuelle Maschine oder den angemeldeten Benutzer. Kontextparameter erscheinen nicht als Eingabefeld und lassen sich deshalb am Formular nicht verändern.

min_role legt fest, wer eine Aktion aufrufen darf. Ein Admin kann eine Aktion mit min_role: operator anlegen, damit jede Station sie auslösen darf, oder mit min_role: planner, damit nur Planer und Admins Zugriff haben.

Wann einsetzen

Anatomie der Definition

Eine Aktion ist ein flaches JSON-Objekt:

Zur Orientierung eine minimale, handgeschriebene Aktion mit einem einzigen Schritt (kein lauffähiges Beispiel, nur die Form):

{
  "name": "beispiel-minimal",
  "summary": "Ein Schritt, ein Kommando.",
  "min_role": "operator",
  "params": [
    {"name": "lot", "kind": "scan", "label": "Los", "source": "operator", "required": true}
  ],
  "steps": [
    {
      "command": "lot.complete",
      "bind": {"lot_id": {"param": "lot"}}
    }
  ]
}

Beispiele

Die ersten beiden Beispiele stammen unverändert aus dem Seed-Profil und zeigen den vollständigen Ablauf, wie er auch in der Demo läuft. Das dritte Beispiel ist eine Aktion zum Nachbauen.

fertigmeldung action
{
  "name": "fertigmeldung",
  "summary": "Fertiges Los an das ERP melden.",
  "min_role": "operator",
  "params": [
    {
      "name": "lot",
      "kind": "text",
      "label": "Los",
      "source": "operator",
      "required": true
    },
    {
      "name": "order",
      "kind": "text",
      "label": "Auftrag",
      "source": "operator",
      "required": true
    },
    {
      "name": "qty",
      "kind": "int",
      "label": "Menge",
      "source": "operator",
      "required": true
    }
  ],
  "steps": [
    {
      "command": "rest.call",
      "bind": {
        "body": {
          "literal": "{\"lot\":\"${lot}\",\"order\":\"${order}\",\"qty\":${qty}}"
        },
        "dest": {
          "literal": "erp"
        },
        "method": {
          "literal": "POST"
        },
        "path": {
          "literal": "/api/finished"
        }
      },
      "no_compensation": "the ERP offers no un-report endpoint for finished goods; a single-step action has no composite invariant to restore"
    }
  ]
}
Mit dem Assistenten
Lege eine Aktion an, die den letzten Schritt eines Auftrags fertig meldet und den Auftrag abschließt.

Die Aktion hat einen einzigen Schritt: einen rest.call an das Zielsystem "erp", der das fertige Los per POST /api/finished meldet. Alle drei Params (Los, Auftrag, Menge) kommen vom Bediener (source: operator). Einen Kontext-Param gibt es nicht, weil die Meldung an keine bestimmte Maschine gebunden ist. Der rest.call ist der letzte Schritt, deshalb trägt er statt eines compensate ein no_compensation: Das ERP kennt keinen Endpunkt, der eine Fertigmeldung zurücknimmt. Der Satz hält fest, warum hier nichts rückgängig gemacht werden kann.

maschine-beladen action
{
  "name": "maschine-beladen",
  "summary": "Charge in die Maschine laden (Material direkt aus dem Lager).",
  "min_role": "operator",
  "params": [
    {
      "name": "machine",
      "kind": "text",
      "label": "Maschine",
      "source": "context",
      "required": true
    },
    {
      "name": "order",
      "kind": "scan",
      "label": "Auftrag",
      "source": "operator",
      "required": true
    },
    {
      "name": "charge",
      "kind": "scan",
      "label": "Charge",
      "source": "operator",
      "required": true
    },
    {
      "name": "qty_kg",
      "kind": "int",
      "label": "Menge (kg)",
      "source": "operator",
      "required": true
    }
  ],
  "steps": [
    {
      "command": "rest.call",
      "bind": {
        "body": {
          "literal": "{\"qty_kg\":${qty_kg},\"order\":\"${order}\",\"machine\":\"${machine}\"}"
        },
        "dest": {
          "literal": "erp"
        },
        "extract": {
          "literal": "{\"unit\":\"$.unit\"}"
        },
        "method": {
          "literal": "POST"
        },
        "path": {
          "literal": "/api/storage/${charge}/consume"
        }
      },
      "compensate": {
        "command": "rest.call",
        "bind": {
          "body": {
            "literal": "{\"qty_kg\":${qty_kg}}"
          },
          "dest": {
            "literal": "erp"
          },
          "method": {
            "literal": "POST"
          },
          "path": {
            "literal": "/api/storage/${charge}/unconsume"
          }
        }
      }
    },
    {
      "command": "lot.create",
      "bind": {
        "charge": {
          "param": "charge"
        },
        "order": {
          "param": "order"
        },
        "qty": {
          "param": "qty_kg"
        },
        "uom": {
          "step": {
            "step": 0,
            "field": "unit"
          }
        }
      }
    },
    {
      "command": "lot.scan-in",
      "bind": {
        "lot_id": {
          "step": {
            "step": 1,
            "field": "id"
          }
        },
        "machine": {
          "param": "machine"
        }
      }
    }
  ]
}
Mit dem Assistenten
Ich brauche eine Aktion fürs Terminal: Kiste scannen, Maschine belegen, Los aktivieren.

Drei Schritte, die zusammengehören. Zuerst bucht ein rest.call den Materialverbrauch beim Zielsystem "erp" ab, also die Entnahme der Menge aus dem Lager. Dieser Schritt ist nicht der letzte und verändert ein externes System, deshalb trägt er ein compensate, das die Entnahme im Fehlerfall wieder rückbucht. Danach legt lot.create das neue Los an (die Mengeneinheit kommt als step-Bindung aus dem ersten Schritt zurück), und lot.scan-in aktiviert es an der Maschine. Die Maschine ist ein Kontext-Param (source: context): Die Station liefert sie automatisch, der Bediener wählt sie nicht aus. Die Params order und charge liest der Bediener per Scan ein. lot.scan-in prüft beim Aktivieren, ob die Maschine die nötige Fähigkeit für dieses Los besitzt, und schlägt sonst fehl. Diese Prüfung sichert die Aktivierung ab, ohne dass die Definition einen eigenen when-Guard braucht.

chargen-abschliessen action
{
  "name": "chargen-abschliessen",
  "summary": "Schließt jede fällige offene Charge und meldet den Abschluss an den Broker.",
  "min_role": "operator",
  "params": null,
  "steps": [
    {
      "command": "query.run",
      "bind": {
        "query": {
          "literal": "offene_chargen_faellig"
        }
      }
    },
    {
      "command": "custom.update",
      "bind": {
        "data": {
          "literal": "{\"status\":\"closed\"}"
        },
        "entity": {
          "literal": "batch"
        },
        "id": {
          "row": "id"
        }
      },
      "for_each": {
        "step": 0,
        "field": "rows"
      }
    },
    {
      "command": "amqp.publish",
      "bind": {
        "body": {
          "literal": "{\"charge_id\":\"${row.id}\"}"
        },
        "dest": {
          "literal": "broker"
        },
        "routing_key": {
          "literal": "charge.abgeschlossen"
        }
      },
      "compensate": {
        "command": "amqp.publish",
        "bind": {
          "body": {
            "literal": "{\"charge_id\":\"${row.id}\"}"
          },
          "dest": {
            "literal": "broker"
          },
          "routing_key": {
            "literal": "charge.abschluss.storno"
          }
        }
      },
      "for_each": {
        "step": 0,
        "field": "rows"
      }
    }
  ]
}
Mit dem Assistenten
Baue eine Aktion, die per query.run alle fälligen offenen Chargen findet und jede einzeln abschließt (for_each).

Diese Aktion zeigt das query.run/for_each-Muster. Der erste Schritt liest mit query.run alle fälligen offenen Chargen. Seine "rows" versorgen zwei nachfolgende for_each-Schritte: einen custom.update, der jede Charge auf "closed" setzt, und einen amqp.publish, der pro Charge den Abschluss an den Broker meldet. Innerhalb der Schleife greift {"row": "…"} pro Zeile auf die Spalten der jeweiligen Charge zu. Der amqp.publish ist zwar der letzte Schritt der Aktion, läuft aber innerhalb der for_each-Schleife und ist unumkehrbar. Deshalb trägt er ein compensate, das die Meldung über einen eigenen Storno-Routing-Key zurücknimmt, falls eine spätere Iteration scheitert, nachdem frühere bereits veröffentlicht haben.

Mit dem Assistenten arbeiten

Jedes Beispiel oben zeigt den Prompt, mit dem Sie dieselbe Aktion über den Assistenten anlegen lassen können. Sie müssen Parameter, Schritte, Bindungen und Bedingungen dann nicht manuell als JSON schreiben. Besonders bei Aktionen achtet der Assistent auf ein fehlendes compensate bei einem unumkehrbaren Schritt und ergänzt es, bevor er den Vorschlag zeigt. Wie der Assistent grundsätzlich arbeitet, beschreibt das Kapitel Der KI-Assistent.

Fallstricke