helixordevelopers

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.

AvailableTier 1 · Developer30 minutesIntermediate

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-pack command. See Installation.
  • Your Developer license at ~/.helixor/helixor.lic. Check it with helixor-pack inspect-license: it must list custom_playbook_compilation under 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 playbookIn a pack you compileStatus
rules entries with type: regex or type: luhn_checksumAll 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 packRun 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_rulesRejected 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#

  1. 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 with action: block, redact_<id> for a rule with action: redact, and permit_clean_payload when nothing matches. Choose rule IDs that read well as actions:

    ActionWhenWhat 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: redact instead when the note may go out with the value replaced by [REDACTED_<ID>] (see Add custom rules).

  2. 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_payload
    

    actions lists 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 its licensed_packs patterns, shown by helixor-pack inspect-license; for a Developer license that is typically custom.*, so this pack is custom.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.

  3. 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 .hxpack as a build artifact, as you would a compiled binary. See Packs.

  4. 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_id names your pack, action is one of the actions you declared, and triggers says which rule fired and what it matched. latency_us varies by machine and covers evaluation only.

  5. Run it on a set of inputs

    summarize.py prints 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
    done
    
    Order 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_pii can 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.
  6. 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 gets hx_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, so clean_text still contains CC-4471. Never send clean_text from a result whose action starts with block, as send_note.py does.

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 codons and hard_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.

Next steps#