Five minutes, end to end

Quickstart

Download two binaries — the CLI and the HTTP API — evaluate the bundled fixture pack against four worked scenarios from basic to advanced, and drive every endpoint from Postman. No .NET install, no DocumentForge required, no schema migration.

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.

macOS & Linux: after unzipping, you may need to mark the binaries executable: 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.

Level 01 · Basic

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.

~70µsfilter / lookup / calc
Level 02 · Filtered

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.

~50µsfilter / constant
Level 03 · Iteration + reference lookup

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 ].

~110µsiterator / mutator / merge
Level 04 · Three-deep nested iteration

Seat assignments per journey × segment × pax

Three nested iterators (journeyssegmentspax) 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.

~190µsnested iteration / merge / lookup

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());
API key auth is opt-in. Set 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.

Why on disk and not in DocumentForge? The Postman collection isn't a rule — it's developer ergonomics. Versioning it alongside the fixtures it exercises means a fresh clone gets working sample requests for every endpoint without an external service. When the rule shape changes, the collection moves with it in the same commit.

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:

01 · Rule shape

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.

02 · Request shape

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.

03 · Worked references

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

  1. 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.schemas setting, IntelliJ JSON Schema mappings, etc.) and you get autocomplete + validation as you type.

  2. 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 productrule-tier-bonus.v1.json
    A markup / surcharge calc on top of a lookuprule-bag-policy.v7.json
    Per-passenger anything (taxes, fees, perks)rule-pnr-taxes.v1.json
    Multi-deep iteration (journey × segment × pax)rule-seat-assignments.v1.json
  3. Declare your inputSchema

    This 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": [ /* … */ ]
    }
  4. 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
    }
  5. Run it with --debug

    .\ruleforge-cli.exe run `
        --endpoint /v1/your/path `
        --request  '@your-request.json' `
        --fixtures fixtures/rules `
        --debug

    The 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 no data.config
    • config-parse-error — config doesn't match the schema
    • arity-violation — wrong number of upstream inputs
    • cycle — directed cycle in the graph
    • missing-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").

The contract round-trips. The engine generates the schemas; you author against them; you validate before publish; the engine validates again at boot. There is no separate SDK to keep in sync, no hand-ported types, no JSON-Schema-to-language codegen step required just to author. Build whatever editor or pipeline you want on top of 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