Write your own decision pack
You define one of your own decisions, whether an order note may be sent to a customer, as a playbook. You compile it into a pack, run it, serve it, and call it from your application. A pack is the unit the platform versions, licenses and serves, so this is the shape every decision you ship takes.
How this differs from Add custom rules
Add custom rules extends the built-in example pack with extra identifiers. This tutorial starts from your own decision instead: you design its action set, compile it under your own pack ID, and treat the result as a service your application depends on. Both use the same compiler.
What you'll build#
- A playbook,
order_notes.yaml, for the decision "may this note go out with the customer's order?". It has three actions: permit the note, or block it because it contains an internal cost code or a supplier reference. - A compiled pack,
order_notes.hxpack, that you run from the command line and serve on loopback. - A client,
send_note.py, that calls the served pack, checks it is talking to the right pack, and fails closed on any action it does not expect.
Prerequisites#
- The runtime installed, which includes the
helixor-packcommand. See Installation. - Your Developer license at
~/.helixor/helixor.lic. Check it withhelixor-pack inspect-license: it must listcustom_playbook_compilationunder features. See Licensing.
What a compiled pack runs in 0.2.1#
The playbook format is ahead of the compiler. Design your pack around what executes today:
| Part of the playbook | In a pack you compile | Status |
|---|---|---|
rules entries with type: regex or type: luhn_checksum | All run on every input, and every match is reported. The first fatal match, in file order, decides. | Available |
| The built-in checks from the example pack | Run on every input in every compiled pack, after your rules. A fatal match of yours decides before a built-in one; with no fatal match, the first warning decides, yours first. | Available |
Your own codons and hard_rules | Rejected by the compiler with UnsupportedPackDeclarationError. Executing them is not supported yet. | Planned |
So in 0.2.1 your own decision logic is a list of regular-expression rules. That is enough for decisions that turn on what a text contains, such as the one below.
Steps#
- Design the decision before the rules
Write down the closed set of outcomes first, because that list is the contract your callers code against. A compiled pack returns
block_<id>for a rule withaction: block,redact_<id>for a rule withaction: redact, andpermit_clean_payloadwhen nothing matches. Choose rule IDs that read well as actions:Action When What the caller does permit_clean_payloadNo rule matches. Send the note. block_internal_cost_codeThe note contains a cost code such as CC-4471.Hold the note. block_supplier_referenceThe note contains a supplier reference such as SUP-QRV-042.Hold the note. Both rules here block, because a note with either value must be held. Use
action: redactinstead when the note may go out with the value replaced by[REDACTED_<ID>](see Add custom rules). - Write the playbook
pack_id: custom.order_notes.v1 version: 1.0.0 name: Outbound order note check kind: internal_policy description: "Decides whether a note may be sent to a customer with their order." actions: - permit_clean_payload - block_internal_cost_code - block_supplier_reference - block_glba_ssn_leakage - block_pci_dss_pan_leakage - block_hipaa_phi_leakage - redact_and_permit_contact_pii rules: - id: internal_cost_code type: regex pattern: '\bCC-\d{4}\b' action: block - id: supplier_reference type: regex pattern: '\bSUP-[A-Z]{3}-\d{3}\b' action: block default_action: permit_clean_payloadactionslists every outcome the pack can return: yours, and the five of the built-in checks that run in every compiled pack. 0.2.1 does not check the list; the next release rejects a list that leaves one out. The pack ID follows<domain>.<name>.v<N>. Your license can only compile and load pack IDs that match itslicensed_packspatterns, shown byhelixor-pack inspect-license; for a Developer license that is typicallycustom.*, so this pack iscustom.order_notes.v1. Quote regex patterns with single quotes so YAML leaves the backslashes alone, and quote any description that contains a colon followed by a space. - Compile it
helixor-pack compile --playbook order_notes.yaml \ --license ~/.helixor/helixor.lic \ --out order_notes.hxpack
Compiling playbook 'order_notes.yaml' locally... Successfully compiled sealed binary pack (890 bytes): order_notes.hxpack
The size depends on your license and the runtime version.
The pack is sealed to your license: it only unseals with the license it was compiled with. Keep the playbook in source control and treat the
.hxpackas a build artifact, as you would a compiled binary. See Packs. - Run it on one input
helixor-pack run --pack order_notes.hxpack \ --license ~/.helixor/helixor.lic \ --text "Restock from SUP-QRV-042 next week."
{ "pack_id": "custom.order_notes.v1", "action": "block_supplier_reference", "invariants_passed": false, "reason": "Pack rule 'supplier_reference' triggered (block)", "triggers": [ { "rule_id": "supplier_reference", "law": "internal_policy", "severity": "FATAL", "matched_items": [ "SUP-QRV-042" ] } ], "remedy": { "clean_text": "[BLOCKED: Prohibited Content]", "redactions_count": 0, "redacted_categories": [] }, "latency_us": 43.42, "tokens_spent": 0, "egress_bytes": 0, "receipt_hash": "hx_proof_1b217c35fe5f69075a97b3ea", "license_status": "ACTIVE", "active_key_id": "ep1_2026", "active_epoch": 1, "edition": "DEVELOPER", "upgrade_notice": null }pack_idnames your pack,actionis one of the actions you declared, andtriggerssays which rule fired and what it matched.latency_usvaries by machine and covers evaluation only. - Run it on a set of inputs
summarize.pyprints the action, the rule IDs and the receipt from each result:import json import sys d = json.load(sys.stdin) print(f" {d['action']} {[t['rule_id'] for t in d['triggers']]} {d['receipt_hash']}")for text in "Order ORD-20931 ships Tuesday." \ "Billed to CC-4471; restock from SUP-QRV-042." \ "Restock from SUP-QRV-042 next week." \ "Order ORD-20931 for ops@example.com, billed to CC-4471."; do echo "$text" helixor-pack run --pack order_notes.hxpack --license ~/.helixor/helixor.lic --text "$text" \ | python summarize.py doneOrder ORD-20931 ships Tuesday. permit_clean_payload [] hx_proof_376e6bd58ba7d40b8d8daacf Billed to CC-4471; restock from SUP-QRV-042. block_internal_cost_code ['internal_cost_code', 'supplier_reference'] hx_proof_adeade9784098a416ecc1b6b Restock from SUP-QRV-042 next week. block_supplier_reference ['supplier_reference'] hx_proof_1b217c35fe5f69075a97b3ea Order ORD-20931 for ops@example.com, billed to CC-4471. block_internal_cost_code ['internal_cost_code', 'RULE-GDPR-EMAIL-REDACT'] hx_proof_25d10d0c0f08d6af9abdeb4a
Three things to notice:
- The second input matches both rules. Both are reported, in file order, and the first one decides the action. Order your rules from most to least important.
- The last input matches your cost-code rule and the built-in email check. Both are reported, and your fatal rule decides. The built-in checks run in every compiled pack, so when none of your rules matches, a built-in action such as
redact_and_permit_contact_piican come back, an action your playbook does not declare. The next step shows how your application handles that. - The third receipt is the one from the single run above. Inputs that the same rule blocks outright share a receipt; see the known issues below.
- Serve it and call it from your application
Serve the pack on loopback so any process on the host can call it:
helixor-pack serve --pack order_notes.hxpack \ --license ~/.helixor/helixor.lic \ --host 127.0.0.1 --port 18734
Your application treats the pack like any other dependency with a contract. It checks that the service has the pack it expects loaded, maps every declared action to what it does next, and fails closed on anything else:
import json import urllib.request SERVICE = "http://127.0.0.1:18734/v1/evaluate" PACK_ID = "custom.order_notes.v1" ACTIONS = {"permit_clean_payload", "block_internal_cost_code", "block_supplier_reference"} def decide(note: str) -> dict: req = urllib.request.Request( SERVICE, data=json.dumps({"text": note}).encode(), headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=2) as resp: d = json.load(resp) if d["pack_id"] != PACK_ID: raise RuntimeError(f"wrong pack loaded: {d['pack_id']}") if d["action"] not in ACTIONS: # An action this pack does not declare, such as a built-in check. # Fail closed: hold the note for a person. return {"send": False, "why": f"unexpected action {d['action']}", "receipt": d["receipt_hash"]} if d["action"].startswith("block_"): return {"send": False, "why": d["reason"], "receipt": d["receipt_hash"]} return {"send": True, "why": "clean", "receipt": d["receipt_hash"]} for note in [ "Order ORD-20931 ships Tuesday.", "Restock from SUP-QRV-042 next week.", "Order ORD-20931 for ops@example.com ships Tuesday.", ]: print(note) print(" ", decide(note))python send_note.py
Order ORD-20931 ships Tuesday. {'send': True, 'why': 'clean', 'receipt': 'hx_proof_376e6bd58ba7d40b8d8daacf'} Restock from SUP-QRV-042 next week. {'send': False, 'why': "Pack rule 'supplier_reference' triggered (block)", 'receipt': 'hx_proof_1b217c35fe5f69075a97b3ea'} Order ORD-20931 for ops@example.com ships Tuesday. {'send': False, 'why': 'unexpected action redact_and_permit_contact_pii', 'receipt': 'hx_proof_9cb68623f6c686e936e953f5'}The third note is held, not sent, because the client refuses any action outside the pack's declared set. The HTTP, WebSocket and other-language clients in Serve decisions to other languages work against your pack unchanged.
Known issues in 0.2.1
- Receipts from a compiled pack hash
clean_text, not the input. When a block rule replaces the whole text with[BLOCKED: Prohibited Content], every input blocked by the same rule gets the same receipt:Ask SUP-ABC-001 about delays.also getshx_proof_1b217c35fe5f69075a97b3ea. Log your own request ID next to it. - A blocked value can stay in
clean_text. The whole text is replaced only when nothing else was redacted. In the last input the email is redacted, soclean_textstill containsCC-4471. Never sendclean_textfrom a result whose action starts withblock, assend_note.pydoes.
Fixed in the next release. Receipts hash the input with the pack version (Receipts), so Ask SUP-QRV-042 about delays. and Ask SUP-ABC-001 about delays. get different receipts. Any fired block rule withholds the whole text, so the last input's clean_text is [BLOCKED: Prohibited Content]. Receipt values differ from the ones shown above. Keep the check in send_note.py.
How it works#
helixor-pack compile parses the playbook, checks that your license is entitled to compile it, and encrypts the compiled form with a key from your license. The pack ID and license ID are bound into the encryption. At run time the CLI verifies the license signature and dates, unseals the pack in memory, and evaluates each input against all of your rules and the built-in checks. Every match is reported; the first fatal one decides, and with no fatal match the first warning decides. Nothing leaves the process while it decides. In your own Python process, HelixorEngine.load_pack("order_notes.hxpack", license_file=...) gives you the same engine.
The service is a thin transport over the same engine. Each POST /v1/evaluate is one in-process evaluation, so the action and receipt are the same as from helixor-pack run.
Limits#
- Your decision logic is regex and Luhn-checksum rules only. Rules over typed facts (numbers, dates, fields of a record) need your own
codonsandhard_rules, which are Planned for compiled packs; the compiler rejects them today. For decisions over typed state today, see the hosted playbooks in Close the learning loop. - Patterns run on every payload with no timeout. Avoid nested quantifiers.
- The flags are in the CLI reference and the keys in the playbook schema.