Some decisions do not belong directly in your source code. Business logic such as request approvals, customer tier classifications, or claim scoring change on their own schedule and are often written by external teams. This guide demonstrates how to isolate such a rule in its own file, supply an input, and receive a decision without allowing the rule to access any other system resources.
The rules are written in Lispex, a deterministic Lisp designed for decision logic where a given rule and input always yield the identical result. Topaz reads each rule during package compilation and follows a prepare-once/evaluate-many runtime model. A prepared rule cannot access files, read the system clock, or connect to the network, and it terminates once it reaches the allocated execution limit. A package built in this manner constitutes a complete-current-profile Lispex decision application, which this page constructs step by step.
This page assumes prior experience building a Topaz package. If you are new to Topaz, start with First Application.
Write the rule
A rule is a standard Lispex source file. The following example compares two numbers and returns one of two string values. Save it as rules/approve.lspx:
(if (< 10 15) "allow" "deny")Three additional rules complete the application package below. The file rules/classify.lspx returns a numeric value rather than a string:
(+ 20 22)The rule rules/deadline-probe.lspx executes indefinitely on its own, providing a workload for the application deadline mechanism to terminate:
(letrec ((loop (lambda () (loop)))) (loop))The fourth rule reuses rules/approve.lspx under a much lower limit, requiring no separate source file.
Set two kinds of limit
Two distinct documents define resource ceilings for execution. A limits document applies to an individual rule, while a quotas document applies to the entire application.
The file rules/approve.limits.json defines the resource ceilings for preparing and evaluating a single rule. A complete document must retain every field, and while lower values may be specified, fields cannot be omitted or added. The following snippet illustrates the two work ceilings:
{
"schema": "topaz.lispex-embed-limits/v1",
"prepare": {
"prepare_work": 1000000
},
"evaluate": {
"eval_work": 10000
}
}The prepare_work and eval_work fields measure evaluator computational effort deterministically rather than tracking elapsed wall-clock time, ensuring that identical rules and inputs consume identical resource units across different host environments.
Every rule requires a dedicated limits document. Both rules/classify.limits.json and rules/deadline-probe.limits.json share this structural format. The document rules/semantic-limit-probe.limits.json uses the same structure with eval_work set to 1, an intentionally insufficient threshold that prevents the rule from completing. This setup allows the application to demonstrate how resource exhaustion is handled.
The file rules/application.quotas.json imposes constraints on the host runtime rather than an individual rule. It permits up to two concurrent evaluations, two queued evaluations, 64 total evaluations, and a finite wall-clock timeout.
The distinction between limits and quotas becomes critical when execution is interrupted. A rule exceeding its eval_work ceiling reaches a semantic limit failure, returning a result type that application code can inspect and pattern-match. Conversely, an application exceeding its global quota rejects the invocation entirely. Because these represent distinct typed outcomes, application logic must distinguish between them.
Declare the package
The manifest configuration connects all components of the application. It selects the Topaz language version and standard library, specifies the native delivery target, declares the complete current Lispex profile and application contract, references the quotas document, and registers each rule. Save it as topaz.toml:
[package]
name = "complete_lispex_decision_application"
version = "0.1.0"
language = "5.19"
entry = "src/main.tpz"
[build]
target = "native"
deterministic = true
[dependencies]
std = "5.19"
[lispex]
profile = "lispex/r7rs-rule-current-profile-bounded/1"
application = "topaz/lispex-decision-application/2"
application_quotas = "rules/application.quotas.json"
[[lispex.rule]]
name = "approve"
source = "rules/approve.lspx"
limits = "rules/approve.limits.json"
[[lispex.rule]]
name = "classify"
source = "rules/classify.lspx"
limits = "rules/classify.limits.json"
[[lispex.rule]]
name = "deadline_probe"
source = "rules/deadline-probe.lspx"
limits = "rules/deadline-probe.limits.json"
[[lispex.rule]]
name = "semantic_limit_probe"
source = "rules/approve.lspx"
limits = "rules/semantic-limit-probe.limits.json"The language and std settings are prescribed by the application contract rather than user-selected, causing toolchains operating in a different language mode to reject the package during preparation. Refer to Toolchain Status for details on the compiler, language mode, and runtime included in an installation.
The profile key selects the exact complete-current-profile evaluator, identified by lispex/r7rs-rule-current-profile-bounded/1. The word bounded in this provider-owned token describes its fixed resource envelope; it does not select the smaller Topaz 5.18 compatibility profile. The application key selects the 5.19 contract controlling how the compiled package is delivered. Both are fixed identifiers rather than configurable parameters.
Each [[lispex.rule]] entry defines three standard fields. The name field maps to a generated function within std.lispex.rules, turning approve into rules.approve(). This identifier is neither a runtime file path nor a component selector. Multiple rule entries can reference the same source file—as demonstrated by approve and semantic_limit_probe—while applying distinct execution limits.
Lock the package
Locking prepares each rule once and records the exact compilation artifacts. Execute the command from the package root:
topaz lock --root .Never enter a component or prepared-artifact digest manually. Inspect topaz.lock prior to committing it to source control. Its [lispex] section locks the profile, application contract, component, evaluator, ABI, value codec, meter, artifact contract, adapter, quotas, target disposition, and catalog of generated handles. Each [[lispex.rule]] row locks the corresponding source, limits, preparation request, submission, and compiled artifact.
The component represents the evaluator itself. The lock pins its state via a cryptographic digest to prevent subsequent builds from silently executing a modified component.
Modifying the manifest, a rule source, a limits document, application quotas, the component, or the profile requires regenerating the lock file. Running a build with the --locked flag rejects any configuration drift instead of silently preparing modified rules.
Call a rule from Topaz code
The lock operation generates the core API within std.lispex alongside a dedicated function for each declared rule inside std.lispex.rules.
let settlement = evaluate(rules.approve(), input, defaultLimits(rules.approve()))
let recorded = evaluateWithEvidence(
rules.approve(),
input,
defaultLimits(rules.approve()),
)The evaluate function returns a settlement value representing the typed result of an evaluation. This settlement indicates whether the rule executed to completion, encountered an internal evaluation failure, or exhausted a resource limit. The defaultLimits function retrieves the execution ceilings recorded in the lock file, preventing calls from requesting higher resource limits than declared by the package.
The input parameter must be a canonical value using a shared byte encoding scheme between the host and evaluator to ensure consistent semantics. Each evaluation initializes a clean guest memory state, global variables, execution meters, and transcript data, ensuring that state is not shared between consecutive rule evaluations.
Use evaluate when only the evaluation result is required. Use evaluateWithEvidence when execution must produce a verifiable audit record alongside the deterministic result.
Store, verify, and replay the evidence
When evaluateWithEvidence resolves to an eligible outcome, it generates a consumer artifact. This artifact encapsulates the evaluation execution, allowing it to be stored, shared, and independently validated without relying on the host application that generated it.
The complete lifecycle consists of six calls:
consumerArtifactBytesserializes the artifact into a byte payload suitable for storage.consumerArtifactFromBytesdeserializes stored bytes while performing validation checks.inspectConsumerArtifactinspects artifact metadata and identities without performing code execution.verifyConsumerArtifactvalidates internal structure, cryptographic digests, and parameter bindings.portableCoreBytesextracts the embedded Lispex-formatted core from the artifact if present.freshReplayre-evaluates the locked rule with the identical input in an isolated guest environment, validating that it produces a matching artifact.
Be careful when evaluating assertions derived from an artifact. The artifact and its portable core are produced client-side without cryptographic authentication. They do not specify an issuing identity, provider authorization, signing authority, component admission, or execution permissions for external actions. Furthermore, operational refusals, cancellations, safety preemptions, and engine failures do not generate a portable core.
Check and run the package
Validate the package before running execution tests, executing both operations against the locked configuration:
topaz check --root . --locked
topaz run --root . --locked -- allPassing the all argument runs a test scenario that exercises the entire application boundary in a single pass. It processes 24 standard inputs, verifies state isolation between rules, triggers semantic resource exhaustion in one rule, and confirms that the application rejects the 65th evaluation attempt after reaching its quota of 64. The reference package implementation produces the following single-line output for all:
all:default:24:evidence:verified:replayed:isolation:2:complete:semantic-limit:exhausted:aggregate-quota:64:refusedAny differing output text or a non-zero exit code signifies a test failure.
The deadline_probe rule is intentionally excluded from the all test suite. Release validation tests handle timeout behaviors separately by duplicating the package, reducing the wall-clock quota from 100 milliseconds to 1 millisecond, updating the lock file, and asserting a DeadlineExceeded result across both interpreter and native release builds. The standard 100-millisecond configuration must then execute successfully. Isolating this test ensures that the output line above remains independent of host CPU performance.
Build the native product
Compile an optimized native executable into an output directory located outside the package root:
topaz build --root . --locked --release --out-dir ../complete-productExecute the binary on the same operating system and hardware architecture used for the build:
../complete-product/target/release/program allOn Windows platforms, execute:
..\complete-product\target\release\program.exe allThe native executable must produce the identical output line. A successful build does not make the generated executable portable across different target architectures.
Where the finished application can run
The application contract specifies a fixed set of supported deployment target routes. Any rejected route is denied prior to code generation or execution, without falling back to the interpreter or native target.
| Route | Disposition |
|---|---|
interpreter | Supported for the locked complete-profile application |
native | Supported only on admitted native release targets |
generated-python | Refused before artifact output |
raw-web | Refused before artifact output |
worker-web | Refused before artifact output |
managed-web | Refused before artifact output |
http-service | Refused before artifact output |
no-capability | Refused before execution or artifact output |
mcp-empty-component-set | Refused before execution |
The Topaz 5.18 restricted-profile contract remains available only as an immutable compatibility route for existing packages. It keeps its own component, profile, contract, and prepared-artifact identity; no selector or fallback can substitute it for the complete-current-profile application described here.