1. Download the bundle
One zip per platform — everything you need for the four scenarios. Self-contained, no .NET runtime needed. Each bundle contains ruleforge-cli, ruleforge-api, the full fixtures/ tree (rules + scenarios + reference sets), and the Postman collection. Unzip and run.
chmod +x ruleforge-cli ruleforge-api. On macOS the first run also needs xattr -d com.apple.quarantine ruleforge-cli ruleforge-api.
2. Unzip and verify
Open a terminal in the folder where you put the binaries and check the CLI runs:
.\ruleforge-cli.exe --help # RuleForge.Cli — runtime + admin # Verbs: run, publish, mirror, bench, schemas
./ruleforge-cli --help # RuleForge.Cli — runtime + admin # Verbs: run, publish, mirror, bench, schemas
The bundle ships with a ready-to-run fixtures/ tree:
. ├─ ruleforge-cli # run / publish / mirror / bench / schemas ├─ ruleforge-api # long-running HTTP engine ├─ fixtures/ │ ├─ rules/ # the four pre-built rule snapshots + bindings │ ├─ scenarios/ # sample request bodies for each scenario │ └─ refs/ # reference sets (rate matrices, lookups) └─ postman/ └─ RuleForge.postman_collection.json
3. Run the four scenarios with the CLI
Each scenario exercises a different combination of node categories, from a simple filter chain to three-deep nested iteration. Pair them with the Node Reference to see exactly which features fire.
Bag policy with markup
Filter on cabin and route, look up base bag fee from ref-price-matrix, calc applies the markup. Returns one structured product with the per-piece bag fee — fee: 517.5 AED for 3 pieces with 15% markup.
Tier bonus eligibility
String filter on $.pax[*].tier with operator in [GOLD, PLAT, IO] and arraySelector: any. Routes to a constant product on pass — { bonusPieces: 1, bonusKg: 5 }. Skip otherwise.
Per-pax PNR tax engine
Iterate $.pax, seed a tax-line shell, stamp paxId from $pax.id, lookup-replace amount from ref-tax-rates by (origin × ageCategory × code), merge with mode collect. Returns [ ADT: £26, CHD: £13 ].
Seat assignments per journey × segment × pax
Three nested iterators (journeys → segments → pax) build a flat tree of seat-eligibility lines, with cabin-class names looked up from ref-cabin-class. Three closing merges collapse back through each scope.
Run them with the CLI
# Level 01 — bag policy with 15% markup ./ruleforge-cli run \ --endpoint /v1/ancillary/bag-policy \ --request @fixtures/scenarios/s-bag-3pc-markup15.json \ --fixtures fixtures/rules \ --debug # → decision: apply, result: { code: BAG, fee: 517.5, currency: AED, ... } # Level 02 — tier bonus eligibility ./ruleforge-cli run \ --endpoint /v1/ancillary/tier-bonus \ --request @fixtures/scenarios/s-gold-pax.json \ --fixtures fixtures/rules # → decision: apply, result: { bonusPieces: 1, bonusKg: 5 } # Level 03 — per-pax PNR tax engine ./ruleforge-cli run \ --endpoint /v1/tax/pnr \ --request @fixtures/scenarios/s-pnr-2pax.json \ --fixtures fixtures/rules \ --debug # → decision: apply, result: [ { paxId: p1, GB1, £26 }, { paxId: p2, GB1, £13 } ] # Level 04 — three-deep nested iteration ./ruleforge-cli run \ --endpoint /v1/seats/assignments \ --request @fixtures/scenarios/s-2j-2s-2p.json \ --fixtures fixtures/rules # → decision: apply, result: nested tree of journey × segment × pax seat lines
# Level 01 — bag policy with 15% markup .\ruleforge-cli.exe run ` --endpoint /v1/ancillary/bag-policy ` --request '@fixtures/scenarios/s-bag-3pc-markup15.json' ` --fixtures fixtures/rules ` --debug # Level 02 — tier bonus eligibility .\ruleforge-cli.exe run ` --endpoint /v1/ancillary/tier-bonus ` --request '@fixtures/scenarios/s-gold-pax.json' ` --fixtures fixtures/rules # Level 03 — per-pax PNR tax engine .\ruleforge-cli.exe run ` --endpoint /v1/tax/pnr ` --request '@fixtures/scenarios/s-pnr-2pax.json' ` --fixtures fixtures/rules ` --debug # Level 04 — three-deep nested iteration .\ruleforge-cli.exe run ` --endpoint /v1/seats/assignments ` --request '@fixtures/scenarios/s-2j-2s-2p.json' ` --fixtures fixtures/rules
4. Or start the HTTP engine
The CLI's run verb is great for one-shot evaluations. For a long-running service that any language can hit, use ruleforge-api. Configuration is via environment variables; the binary is a standard ASP.NET Core executable, so --urls picks the listening address.
$env:RULEFORGE_FIXTURES_DIR = ".\fixtures\rules" $env:RULEFORGE_REFS_DIR = ".\fixtures\refs" .\ruleforge-api.exe --urls http://localhost:5050 # info: Bound POST /v1/ancillary/bag-policy -> rule-bag-policy@7 # info: Bound POST /v1/ancillary/tier-bonus -> rule-tier-bonus@1 # info: Bound POST /v1/tax/pnr -> rule-pnr-taxes@1 # info: Bound POST /v1/seats/assignments -> rule-seat-assignments@1 # info: Now listening on: http://localhost:5050
RULEFORGE_FIXTURES_DIR=./fixtures/rules \ RULEFORGE_REFS_DIR=./fixtures/refs \ ./ruleforge-api --urls http://localhost:5050 # info: Bound POST /v1/ancillary/bag-policy -> rule-bag-policy@7 # info: Bound POST /v1/ancillary/tier-bonus -> rule-tier-bonus@1 # info: Bound POST /v1/tax/pnr -> rule-pnr-taxes@1 # info: Bound POST /v1/seats/assignments -> rule-seat-assignments@1 # info: Now listening on: http://localhost:5050
Hit any of the bound endpoints from any HTTP client. Append ?debug=true (or send X-Debug: true) to get the per-node trace.
using System.Net.Http.Json; var http = new HttpClient { BaseAddress = new("http://localhost:5050") }; // Level 03 — per-pax PNR tax var resp = await http.PostAsJsonAsync("v1/tax/pnr?debug=true", new { pnr = "TAX001", orig = "LHR", taxCode = "GB1", pax = new[] { new { id = "p1", ageCategory = "ADT" }, new { id = "p2", ageCategory = "CHD" }, } }); Console.WriteLine(await resp.Content.ReadAsStringAsync());
# Level 03 — per-pax PNR tax curl -X POST "http://localhost:5050/v1/tax/pnr?debug=true" \ -H "Content-Type: application/json" \ --data @fixtures/scenarios/s-pnr-2pax.json
var http = HttpClient.newHttpClient(); var resp = http.send(HttpRequest.newBuilder() .uri(URI.create("http://localhost:5050/v1/tax/pnr?debug=true")) .header("Content-Type", "application/json") .POST(BodyPublishers.ofFile(Path.of("fixtures/scenarios/s-pnr-2pax.json"))) .build(), BodyHandlers.ofString()); System.out.println(resp.body());
import json, httpx with open("fixtures/scenarios/s-pnr-2pax.json") as f: body = json.load(f) resp = httpx.post( "http://localhost:5050/v1/tax/pnr", params={"debug": "true"}, json=body, ) print(resp.json())
import fs from "node:fs/promises"; const body = JSON.parse(await fs.readFile("fixtures/scenarios/s-pnr-2pax.json", "utf8")); const resp = await fetch("http://localhost:5050/v1/tax/pnr?debug=true", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); console.log(await resp.json());
RULEFORGE_API_KEY=secret before launch and the engine starts rejecting requests without X-AERO-Key: secret (or Authorization: Bearer secret). /health stays open for liveness probes.
5. Or just import the Postman collection
Don't want to write code yet? Drop the included Postman collection on top of a running ruleforge-api and click your way through every endpoint — health, the four scenarios, debug mode, admin operations. The collection lives on the file system alongside the fixtures, not in DocumentForge — so you can version it next to the rules it exercises.
In Postman: File → Import → drop postman/RuleForge.postman_collection.json from the bundle (or download from this page if you don't have the bundle handy). Set baseUrl to http://localhost:5050 and apiKey to whatever you started the engine with (leave empty if no auth). Run Health → Liveness first, then any of the Scenarios — the test scripts assert decision === "apply" as a smoke test.
6. Author your own rule
The binary is self-describing. With just the binary you can build new rules and conform to the schema the engine actually validates against — no separate SDK, no external doc, no drift. Three layers make this possible:
11 JSON Schemas, dumped from the binary
Generated from the live C# runtime types via JsonSchemaExporter — they are the contract. Validate with any off-the-shelf JSON Schema validator (ajv, jsonschema, NJsonSchema). If it passes, the engine accepts it.
Every rule carries its own inputSchema
Open any rule JSON and the inputSchema field declares exactly what request body the engine will accept. Use it to render forms, codegen client types, or drive contract tests downstream.
Four bundled rules cover 8 of 11 node categories
rule-bag-policy · rule-tier-bonus · rule-pnr-taxes · rule-seat-assignments. Copy the closest one as your starting point — they're all valid, runnable JSON.
Step-by-step
-
Dump the contracts
.\ruleforge-cli.exe schemas --out .\schemas # wrote 11 schemas: # rule.schema.json ← the top-level Rule object # envelope.schema.json ← the response shape # string-filter-config.schema.json ← per-node-type configs # number-filter-config.schema.json # date-filter-config.schema.json # mutator-config.schema.json # calc-config.schema.json # iterator-config.schema.json # merge-config.schema.json # reference-config.schema.json # sub-rule-call.schema.json
Wire the schemas into your editor (VS Code's
json.schemassetting, IntelliJ JSON Schema mappings, etc.) and you get autocomplete + validation as you type. -
Copy the closest existing rule as a starting point
Don't start from a blank file. Pick whichever bundled rule is closest to what you want and edit it:
If you want… Start from An eligibility filter that emits a fixed product rule-tier-bonus.v1.jsonA markup / surcharge calc on top of a lookup rule-bag-policy.v7.jsonPer-passenger anything (taxes, fees, perks) rule-pnr-taxes.v1.jsonMulti-deep iteration (journey × segment × pax) rule-seat-assignments.v1.json -
Declare your
inputSchemaThis is what makes the rule self-describing for downstream callers. The engine doesn't enforce it (yet) but every published rule should carry one — it's how UIs render forms and how clients know what to POST:
{ "id": "rule-my-thing", "name": "Description for humans", "endpoint": "/v1/your/path", "method": "POST", "status": "published", "currentVersion": 1, "inputSchema": { "type": "object", "properties": { "orig": { "type": "string" }, "pax": { "type": "array" } }, "required": ["orig", "pax"] }, "outputSchema": { /* what your rule produces */ }, "nodes": [ /* … */ ], "edges": [ /* … */ ] } -
Bind your endpoint
Add an entry to
fixtures/rules/_endpoint-bindings.json:{ "POST /v1/ancillary/bag-policy": "rule-bag-policy@7", "POST /v1/ancillary/tier-bonus": "rule-tier-bonus@1", "POST /v1/your/path": "rule-my-thing@1" // ← new } -
Run it with
--debug.\ruleforge-cli.exe run ` --endpoint /v1/your/path ` --request '@your-request.json' ` --fixtures fixtures/rules ` --debugThe trace shows every node's verdict and output. If the rule is invalid the envelope returns
decision: "error"with one of the eight stable error categories — see Reference / Error categories:missing-config— a node has nodata.configconfig-parse-error— config doesn't match the schemaarity-violation— wrong number of upstream inputscycle— directed cycle in the graphmissing-rule·missing-reference-set·missing-source·legacy-config-shape
UIs can switch on these to distinguish bugs ("engine should have handled this") from authoring lints ("rule isn't quite right yet").
ruleforge-cli schemas.
7. Embed as a library (.NET)
If your service is .NET, skip the REST hop entirely and use RuleForge as an in-process library. The same fixture pack, the same envelope, but with sub-microsecond function-call latency instead of HTTP.
dotnet add package RuleForge.Core dotnet add package RuleForge.DocumentForge
using RuleForge.Core; using RuleForge.Core.Loader; using RuleForge.Core.Graph; // Load the fixture pack var ruleSource = new LocalFileRuleSource("./fixtures/rules"); var refSource = new LocalFileReferenceSetSource("./fixtures/refs"); var runner = new RuleRunner(); // Resolve and run var rule = await ruleSource.GetByEndpointAsync("/v1/ancillary/bag-policy", HttpMethodKind.POST); var request = JsonDocument.Parse(await File.ReadAllTextAsync("./fixtures/scenarios/s-bag-3pc-markup15.json")); var envelope = await runner.RunAsync( rule!, request.RootElement, new RuleRunner.Options(Debug: true, SubRuleSource: ruleSource, ReferenceSetSource: refSource)); Console.WriteLine(envelope.Decision); // "apply" Console.WriteLine(envelope.Result); // { code: "BAG", fee: 517.5, ... }
8. Switch to DocumentForge
The local file pack is great for dev and CI. For staging and prod, point the engine at DocumentForge — same envelope, same rule shape, same scenarios. The engine resolves endpoint + method → ruleId → version → snapshot on first call and caches each step.
# DocumentForge-backed serve RULEFORGE_RULE_SOURCE=df \ RULEFORGE_DF_BASE_URL=http://localhost:5000 \ RULEFORGE_DF_API_KEY=your-key \ RULEFORGE_ENV=staging \ ./ruleforge-api --urls http://localhost:5050 # Push your fixture pack into DocumentForge in one command: ./ruleforge-cli mirror \ --from ./fixtures \ --to df://localhost:5000 \ --api-key your-key
See Storage for the full source-switching matrix and Deployment for co-located DocumentForge topology.
Next steps
- Use Cases — why RuleForge for IATA Modern Airline Retailing.
- Node Reference — every node category with JSON Schemas, examples, and error states.
- Rule Schema — the JSON shape of a rule, top to bottom.
- Performance — full benchmark methodology and the 70µs hot path.
- GitHub — source, issues, releases.