JRL (JuLC Rule Language) Guide (Experimental)
Write Cardano smart contracts using a simple, declarative rule language — no Java knowledge required. JRL compiles to UPLC through JuLC, producing the same efficient Plutus V3 scripts as hand-written Java validators.
What is JRL?
Section titled “What is JRL?”JRL is a domain-specific language inspired by business rule engines like Drools. Instead of writing imperative code, you declare rules with conditions and actions. The JRL compiler translates your rules into Java, then compiles that Java to UPLC via the JuLC compiler pipeline.
Key design choices:
- Non-Turing-complete — no loops, no recursion, no mutable variables
- Declarative — say what should happen, not how
- Safe by default — the type checker catches errors before compilation
- Familiar syntax — reads like English:
when ... then allow
Compilation pipeline:
YourContract.jrl → parse → type check → Java source → JulcCompiler → UPLC bytecodeQuick Start with julc CLI
Section titled “Quick Start with julc CLI”The fastest way to start writing JRL contracts is with the julc command-line tool.
1. Install julc
Section titled “1. Install julc”# macOS / Linuxbrew install bloxbean/tap/julc
# Or download from GitHub Releases:# https://github.com/bloxbean/julc/releases2. Create a new JRL project
Section titled “2. Create a new JRL project”julc new my-contract --jrlcd my-contractThis creates:
my-contract/├── julc.toml # project config├── src/│ └── AlwaysSucceeds.jrl # starter validator├── test/│ └── AlwaysSucceedsTest.java└── .julc/ └── stdlib/ # auto-installed standard libraryThe generated AlwaysSucceeds.jrl:
contract "AlwaysSucceeds"version "1.0"purpose spending
rule "Always allow"when Condition( true )then allow
default: deny3. Build
Section titled “3. Build”julc buildOutput:
Building my-contract ... Compiling AlwaysSucceeds.jrl ... OK [67 bytes, a1b2c3d4...]
Build successful: 1 validator(s) compiled to build/plutus/The compiled script is in build/plutus/AlwaysSucceeds.uplc, and a CIP-57
blueprint is generated at build/plutus/plutus.json.
Contract Structure
Section titled “Contract Structure”Every JRL file follows this structure:
contract "<name>"version "<version>"purpose <spending | minting | withdraw | certifying | voting | proposing>
[params: ...] -- optional: compile-time parameters[datum <Type>: ...] -- optional: datum schema (spending only)[record <Type>: ...] -- optional: helper record types[redeemer <Type>: ...] -- optional: redeemer schema
rule "<name>" -- one or more ruleswhen <patterns>then <allow | deny>
default: <allow | deny> -- fallback when no rule matchesHeader
Section titled “Header”The header declares the contract’s name, version, and purpose:
contract "Vesting" version "1.0" purpose spendingPurpose determines what kind of Cardano script is generated:
| Purpose | Cardano Script Type | Has Datum? |
|---|---|---|
spending | Spending validator | Yes |
minting | Minting policy | No |
withdraw | Withdrawal validator | No |
certifying | Certifying validator | No |
voting | Voting validator | No |
proposing | Proposing validator | No |
JRL has a focused set of types that map to Cardano on-chain representations:
| JRL Type | Description | Java Equivalent |
|---|---|---|
Integer | Arbitrary-precision integer | BigInteger |
Lovelace | ADA amount (1 ADA = 1M) | BigInteger |
POSIXTime | Unix timestamp (milliseconds) | BigInteger |
ByteString | Raw bytes | byte[] |
PubKeyHash | Public key hash | byte[] |
PolicyId | Minting policy ID | byte[] |
TokenName | Token/asset name | byte[] |
ScriptHash | Script hash | byte[] |
DatumHash | Datum hash | byte[] |
TxId | Transaction ID | byte[] |
Address | Cardano address | Address |
Text | UTF-8 string | String |
Boolean | True/false | boolean |
List <T> | Typed list | List<T> |
Optional <T> | Optional value | Optional<T> |
Parameters
Section titled “Parameters”Parameters are compile-time values baked into the script. They make your contract reusable — deploy the same logic with different keys, deadlines, or thresholds.
contract "TimeLock" version "1.0" purpose spendingparams: owner : PubKeyHash lockTime : POSIXTimeParameters are referenced by name in rules:
rule "Owner can spend after lock"when Transaction( signedBy: owner ) Transaction( validAfter: lockTime )then allowWhen building with julc build, parameter values are applied at deployment time
via the CIP-57 blueprint.
Datum Declaration
Section titled “Datum Declaration”Spending validators can declare a datum — structured data stored at the UTxO:
datum VestingDatum: owner : PubKeyHash beneficiary : PubKeyHash deadline : POSIXTimeThis declares a record type. In rules, you extract fields using the Datum
pattern with $variable bindings:
rule "Beneficiary withdraws after deadline"when Datum( VestingDatum( beneficiary: $ben, deadline: $dl ) ) Transaction( signedBy: $ben ) Transaction( validAfter: $dl )then allowRecord Declarations
Section titled “Record Declarations”You can declare helper record types for organizing complex data:
record Payment: recipient : PubKeyHash amount : LovelaceRecords can be used as datum field types, redeemer field types, or anywhere a structured type is needed.
Redeemer Declaration
Section titled “Redeemer Declaration”The redeemer tells the validator what action the transaction wants to perform. JRL supports two styles:
Record redeemer (single action with data)
Section titled “Record redeemer (single action with data)”redeemer SpendAction: amount : Lovelace deadline : POSIXTimeVariant redeemer (multiple actions)
Section titled “Variant redeemer (multiple actions)”redeemer MintAction: | Mint | Burn | Transfer: recipient : PubKeyHash amount : IntegerVariant redeemers use | to declare alternatives. Each variant can have zero or
more fields. In rules, you match specific variants:
rule "Authority can mint"when Redeemer( Mint ) Transaction( signedBy: authority )then allow
rule "Owner can burn"when Redeemer( Burn ) Transaction( signedBy: owner )then allowRules are the heart of JRL. Each rule has:
- A name (for documentation and tracing)
- Patterns in the
whenclause (conditions that must all be true) - An action (
allowordeny)
rule "Owner can always withdraw"when Datum( VestingDatum( owner: $owner ) ) Transaction( signedBy: $owner )then allowRules are evaluated in order. The first rule whose conditions all match
determines the result. If no rule matches, the default action applies.
Evaluation semantics
Section titled “Evaluation semantics”for each rule (in declaration order): if ALL patterns in the rule match: return the rule's action (allow → true, deny → false)return default actionFact Patterns
Section titled “Fact Patterns”Patterns appear in a rule’s when clause. All patterns in a rule must match for
the rule to fire. JRL provides eight pattern types:
Datum( … )
Section titled “Datum( … )”Extract and match datum fields (spending validators only):
-- Bind a field to a variableDatum( MyDatum( owner: $owner ) )
-- Match a literal valueDatum( MyDatum( flag: true ) )
-- Bind multiple fieldsDatum( MyDatum( owner: $o, amount: $a, deadline: $d ) )Redeemer( … )
Section titled “Redeemer( … )”Match redeemer type and extract fields:
-- Simple variant match (no fields)Redeemer( Mint )
-- Variant with field extractionRedeemer( Transfer( recipient: $r, amount: $a ) )
-- Record redeemer field extractionRedeemer( SpendAction( amount: $amt ) )Transaction( field: expr )
Section titled “Transaction( field: expr )”Check transaction properties:
-- Check if a public key signed the transactionTransaction( signedBy: owner )Transaction( signedBy: $boundVar )
-- Check transaction validity intervalTransaction( validAfter: lockTime ) -- tx valid range starts after lockTimeTransaction( validBefore: deadline ) -- tx valid range ends before deadline
-- Bind the transaction fee to a variableTransaction( fee: $txFee )The fee: field binds the transaction fee to a variable for use in conditions:
rule "Fee must be reasonable"when Transaction( fee: $f ) Condition( $f <= 2000000 )then allowCondition( expr )
Section titled “Condition( expr )”Arbitrary boolean expression:
Condition( true )Condition( $amount > 1000000 )Condition( sha2_256($secret) == expectedHash )Condition( $a + $b >= $threshold )Output( … )
Section titled “Output( … )”Check transaction outputs by address, value, and/or datum:
-- Output must go to address with minimum ADAOutput( to: $recipient, value: minADA( $amount ) )
-- Output must contain a specific tokenOutput( to: $addr, value: contains( $policy, $token, 1 ) )
-- Output must have a specific inline datumOutput( to: receiver, Datum: inline PaymentReceipt( payer: $payer ) )
-- Output with both value and datum checksOutput( to: receiver, value: minADA( 2000000 ), Datum: inline Receipt( ref: refHash ) )The Datum: field checks the inline datum at the output address. Field values
are compared for equality.
Input( … )
Section titled “Input( … )”Check transaction inputs — useful for verifying the spending UTxO or checking that specific tokens are present in the transaction inputs:
-- Bind the value at the script's own inputInput( from: ownAddress, value: $inputVal )
-- Check that inputs contain a specific token (authorization pattern)Input( token: contains( authPolicy, "AUTH", 1 ) )Supported fields:
| Field | Description |
|---|---|
from: | Address expression (ownAddress or a parameter/variable) |
value: | Bind own input’s value to a $variable (requires from: ownAddress) |
token: | Check that total input value contains a token: contains(policy, token, amount) or minADA(amount) |
Notes:
value: $varbinds the value of the script’s own input UTxO, and requiresfrom: ownAddress.token:checks the totalvalueSpentof all transaction inputs.
Mint( … )
Section titled “Mint( … )”Check minting or burning activity in the transaction:
-- Bind minted amount to a variableMint( policy: ownPolicyId, token: "MyToken", amount: $amt )
-- Check that tokens are being burnedMint( policy: ownPolicyId, token: "LPToken", burned )
-- Check that a specific token is being minted (amount > 0)Mint( policy: ownPolicyId, token: "Ticket" )Supported fields:
| Field | Description |
|---|---|
policy: | Policy ID expression (required) |
token: | Token name expression |
amount: | Bind minted amount to a $variable |
burned | Assert the minted amount is negative (burning) |
Constraints:
policy:is required (error JRL041 if missing).burnedandamount:are mutually exclusive (error JRL045).- At least one of
token:,amount:, orburnedmust be present (error JRL044).
ContinuingOutput( … )
Section titled “ContinuingOutput( … )”Check outputs that go back to the script’s own address. This is the “state continuation” pattern — verifying that the contract re-creates itself with updated state:
-- Check minimum ADA in continuing outputContinuingOutput( value: minADA( 2000000 ) )
-- Check continuing output has specific datumContinuingOutput( Datum: inline StateDatum( counter: 42 ) )
-- Check both value and datumContinuingOutput( value: minADA( 2000000 ), Datum: inline StateDatum( counter: $newCount ) )
-- Check continuing output contains a specific tokenContinuingOutput( value: contains( policy, token, 1 ) )Notes:
- Only valid in
spendingpurpose (error JRL042 otherwise). - Uses
getContinuingOutputs(ctx)— outputs that go back to the same script address. Noto:field needed since the address is implicit. - Checks the first continuing output.
Expressions
Section titled “Expressions”JRL expressions appear inside patterns and conditions. They support:
Arithmetic
Section titled “Arithmetic”$a + $b$a - $b$a * $bComparison
Section titled “Comparison”$a == $b -- equality$a != $b -- inequality$a > $b -- greater than$a >= $b -- greater or equal$a < $b -- less than$a <= $b -- less or equalLogical
Section titled “Logical”$a && $b -- logical AND$a || $b -- logical OR!$a -- logical NOTField access
Section titled “Field access”$datum.owner$datum.deadlineVariable references
Section titled “Variable references”Variables are bound in Datum and Redeemer patterns using $name syntax.
Parameters are referenced by their plain name (no $ prefix).
-- $owner is a bound variable from Datum pattern-- authority is a parameterTransaction( signedBy: $owner )Transaction( signedBy: authority )Literals
Section titled “Literals”42 -- integer"hello" -- string0xFF -- hex bytestrue / false -- booleanBuilt-in functions
Section titled “Built-in functions”| Function | Description | Example |
|---|---|---|
sha2_256 | SHA-256 hash | sha2_256($secret) |
blake2b_256 | BLAKE2b-256 hash | blake2b_256($data) |
sha3_256 | SHA3-256 hash | sha3_256($input) |
length | List/bytestring length | length($signatories) |
Special references
Section titled “Special references”| Reference | Description |
|---|---|
ownAddress | The script’s own address |
ownPolicyId | The script’s own policy ID (minting) |
Default Action
Section titled “Default Action”Every contract (or purpose section) must end with a default action:
default: deny -- reject if no rule matched (most common)default: allow -- accept if no rule matchedBest practice: Use default: deny to fail-closed. Only use default: allow
when you explicitly want permissive behavior.
Trace Messages
Section titled “Trace Messages”Add optional trace messages to rules for debugging:
rule "Owner can withdraw"when Transaction( signedBy: owner )then allow trace "owner-withdraw"Trace messages appear in transaction logs during script evaluation, useful for debugging failed validations.
Comments
Section titled “Comments”-- This is a single-line comment
/* This is a multi-line comment */Complete Examples
Section titled “Complete Examples”Example 1: Simple Transfer (Beginner)
Section titled “Example 1: Simple Transfer (Beginner)”The simplest useful validator — only the designated receiver can spend the UTxO:
contract "SimpleTransfer" version "1.0" purpose spendingparams: receiver : PubKeyHash
rule "Receiver can spend"when Transaction( signedBy: receiver )then allow
default: denyExample 2: Time Lock (Parameters + Time)
Section titled “Example 2: Time Lock (Parameters + Time)”Lock funds until a specific time. Only the owner can spend, and only after the lock expires:
contract "TimeLock" version "1.0" purpose spendingparams: owner : PubKeyHash lockTime : POSIXTime
rule "Owner can spend after lock time"when Transaction( signedBy: owner ) Transaction( validAfter: lockTime )then allow
default: denyExample 3: Vesting (Datum + Multiple Rules)
Section titled “Example 3: Vesting (Datum + Multiple Rules)”Two spending paths: the owner can always withdraw, but the beneficiary can only withdraw after the deadline:
contract "Vesting" version "1.0" purpose spending
datum VestingDatum: owner : PubKeyHash beneficiary : PubKeyHash deadline : POSIXTime
rule "Owner can always withdraw"when Datum( VestingDatum( owner: $owner ) ) Transaction( signedBy: $owner )then allow
rule "Beneficiary can withdraw after deadline"when Datum( VestingDatum( beneficiary: $ben, deadline: $deadline ) ) Transaction( signedBy: $ben ) Transaction( validAfter: $deadline )then allow
default: denyExample 4: Multi-Signature Treasury (Two Signers)
Section titled “Example 4: Multi-Signature Treasury (Two Signers)”Both signers stored in the datum must sign the transaction:
contract "MultiSigTreasury" version "1.0" purpose spending
datum TreasuryDatum: signer1 : PubKeyHash signer2 : PubKeyHash
rule "Both signers required"when Datum( TreasuryDatum( signer1: $s1, signer2: $s2 ) ) Transaction( signedBy: $s1 ) Transaction( signedBy: $s2 )then allow
default: denyExample 5: HTLC — Hash Time-Locked Contract (Variant Redeemer + Crypto)
Section titled “Example 5: HTLC — Hash Time-Locked Contract (Variant Redeemer + Crypto)”Funds can be claimed by guessing a secret (hash preimage), or reclaimed by the owner after expiry:
contract "HTLC" version "1.0" purpose spendingparams: secretHash : ByteString expiration : POSIXTime owner : PubKeyHash
redeemer HtlcAction: | Guess: answer : ByteString | Withdraw
rule "Correct guess unlocks"when Redeemer( Guess( answer: $answer ) ) Condition( sha2_256($answer) == secretHash )then allow
rule "Owner can withdraw after expiry"when Redeemer( Withdraw ) Transaction( signedBy: owner ) Transaction( validAfter: expiration )then allow
default: denyExample 6: Multi-Sig Minting Policy (Minting + Variant Redeemer)
Section titled “Example 6: Multi-Sig Minting Policy (Minting + Variant Redeemer)”A minting policy with three paths: authority mint, owner burn, or multi-sig mint:
contract "MultiSigMinting" version "1.0" purpose mintingparams: authority : PubKeyHash owner : PubKeyHash cosigner1 : PubKeyHash cosigner2 : PubKeyHash
redeemer MintAction: | MintByAuthority | BurnByOwner | MintByMultiSig
rule "Authority can mint"when Redeemer( MintByAuthority ) Transaction( signedBy: authority )then allow
rule "Owner can burn"when Redeemer( BurnByOwner ) Transaction( signedBy: owner )then allow
rule "Multi-sig can mint"when Redeemer( MintByMultiSig ) Transaction( signedBy: cosigner1 ) Transaction( signedBy: cosigner2 )then allow
default: denyExample 7: Output Checking (Value Constraints)
Section titled “Example 7: Output Checking (Value Constraints)”Ensure the transaction pays the right amount to the right address:
contract "OutputCheck" version "1.0" purpose spending
datum PaymentDatum: recipient : ByteString minAmount : Lovelace
rule "Payment meets minimum"when Datum( PaymentDatum( recipient: $addr, minAmount: $min ) ) Output( to: $addr, value: minADA( $min ) )then allow
default: denyMulti-Validator Contracts
Section titled “Multi-Validator Contracts”A single JRL file can define multiple validator purposes using purpose sections.
This creates a multi-validator script that handles spending, minting, or other
purposes in one contract:
contract "TokenMarket" version "1.0"
purpose spending: redeemer SpendAction: | Buy | Withdraw
rule "Anyone can buy" when Redeemer( Buy ) Condition( true ) then allow
rule "Owner withdraws" when Redeemer( Withdraw ) Transaction( signedBy: owner ) then allow
default: deny
purpose minting: redeemer MintAction: | Mint | Burn
rule "Authority mints" when Redeemer( Mint ) Transaction( signedBy: authority ) then allow
default: denyEach purpose section has its own redeemer, rules, and default action. Shared parameters and datum declarations go at the top level.
Error Messages
Section titled “Error Messages”The JRL compiler provides clear error messages with source locations and
suggestions. Errors are prefixed with codes like JRL001:
| Code | Category | Example |
|---|---|---|
JRL000 | Syntax error | Missing keyword or unexpected token |
JRL001 | Missing default | Contract has no default: action |
JRL002 | Missing rules | Contract or section has no rules |
JRL003 | Duplicate name | Two rules with the same name |
JRL005 | Unknown type | Field uses an undefined type |
JRL007 | Duplicate field | Record has two fields with the same name |
JRL008 | Duplicate variant | Redeemer has two variants with the same name |
JRL009 | Missing header | Contract name or version not specified |
JRL010 | Missing purpose | No purpose in header and no purpose sections |
JRL011 | Unbound variable | $var used in condition but never bound |
JRL041 | Mint missing policy | Mint pattern requires policy: field |
JRL042 | Wrong purpose | ContinuingOutput only valid in spending |
JRL044 | Mint incomplete | Mint needs token:, amount:, or burned |
JRL045 | Mutually exclusive | burned and amount: cannot both appear |
JRL046 | Input constraint | value: $var requires from: ownAddress |
JRL047 | Input useless | Input(from: expr) alone has no effect |
JRL050 | Datum literal | Literal values not allowed in datum patterns |
JRL vs Java — When to Use Which
Section titled “JRL vs Java — When to Use Which”| Use JRL when… | Use Java when… |
|---|---|
| Authorization logic (signatures + time) | Complex business logic with loops |
| Output/input/minting checks | Custom data transformations |
| Pattern matching on datum/redeemer | Multiple helper methods |
| State continuation (ContinuingOutput) | Fine-grained budget optimization |
| Quick prototyping | Integration with off-chain Java code |
| Non-developers writing validators | Advanced stdlib usage (maps, lists, HOFs) |
| Clear, auditable rule sets | Complex multi-step state machines |
JRL is perfect for validators that follow the pattern: “check conditions, allow or deny.” For complex logic involving iteration, accumulation, or deep data manipulation, use Java directly.
You can mix both in the same project — julc build compiles .java and .jrl
files side by side.
How JRL Compiles
Section titled “How JRL Compiles”Understanding the compilation pipeline helps with debugging:
┌─────────────┐ Vesting.jrl ───→│ JRL Parser │───→ AST (ContractNode) └──────┬──────┘ │ ┌──────▼──────┐ │ Type Checker │───→ Diagnostics (errors/warnings) └──────┬──────┘ │ ┌──────▼──────────┐ │ Java Transpiler │───→ Vesting.java (generated) └──────┬──────────┘ │ ┌──────▼──────────┐ │ JulcCompiler │───→ UPLC Program └──────┬──────────┘ │ ┌──────▼──────────┐ │ CBOR + FLAT │───→ Plutus script bytes └─────────────────┘- Parser: ANTLR4-based parser converts JRL syntax into an AST
- Type Checker: Validates structure, types, names, and variable scoping
- Java Transpiler: Generates a JuLC-compatible Java validator class
- JulcCompiler: Compiles the generated Java to UPLC bytecode
- Serialization: UPLC is serialized to on-chain format
The intermediate Java source is available for inspection — useful for debugging or understanding what JRL generates.
Programmatic Usage
Section titled “Programmatic Usage”You can use the JRL compiler directly from Java code:
Full compilation (JRL → UPLC)
Section titled “Full compilation (JRL → UPLC)”var compiler = new JrlCompiler();var result = compiler.compile(jrlSource, "MyContract.jrl");
if (result.hasErrors()) { for (var diag : result.jrlDiagnostics()) { System.err.println(diag); }} else { var program = result.compileResult().program(); // Use the UPLC program...}Transpile only (JRL → Java source)
Section titled “Transpile only (JRL → Java source)”var compiler = new JrlCompiler();var transpiled = compiler.transpile(jrlSource, "MyContract.jrl");
if (!transpiled.hasErrors()) { String javaSource = transpiled.javaSource(); System.out.println(javaSource);}Parse and type check
Section titled “Parse and type check”var compiler = new JrlCompiler();var diagnostics = compiler.check(jrlSource, "MyContract.jrl");
for (var diag : diagnostics) { if (diag.isError()) { System.err.println(diag.code() + ": " + diag.message()); }}Gradle/Maven dependency
Section titled “Gradle/Maven dependency”implementation 'com.bloxbean.cardano:julc-jrl-core:<version>'Tips and Best Practices
Section titled “Tips and Best Practices”-
Start with
default: deny— fail-closed is safer. Only allow what you explicitly permit. -
Order rules by specificity — put more specific rules (variant redeemer matches) before general ones (plain conditions).
-
Use parameters for keys and deadlines — don’t hardcode public key hashes or timestamps. Parameters make your contract reusable.
-
Bind only what you need — in datum/redeemer patterns, only bind fields you actually use in conditions.
-
One rule per logical path — each rule should represent one clear spending or minting scenario. Multiple patterns in a rule mean AND; multiple rules mean OR.
-
Test with julc — the CLI compiles and validates your contract instantly, catching type errors and structural issues before deployment.
-
Inspect generated Java — use the
transpileAPI to see what Java code your JRL produces. This helps understand the mapping and debug issues.