Skip to main content
penscript supports local variable definitions, deferred evaluation, and value pipelines. These operators let you break complex expressions into readable parts, build reusable templates, and chain transformations step by step.

Local Variables: :define + :in

Defines local variables that are available only within the :in expression. Variables can reference each other sequentially — each definition can use variables defined before it.
{
  ":define": [
    { "t": "{s} {s}" },
    { "u": "{t} {t}" }
  ],
  ":in": "{u}"
}

// Scope: { "s": "hello" }
// Result: "hello hello hello hello"
Variables can be computed using operators:
{
  ":define": [
    { "base_premium": { ":product": [0.7355, "{data.horses_power}"] } },
    { "tax": { ":multiply": ["{base_premium}", 0.21] } },
    { "total": { ":sum": ["{base_premium}", "{tax}"] } }
  ],
  ":in": "Total premium: {total}"
}

// Scope: { "data": { "horses_power": 110 } }
// Result: "Total premium: 97.7"
This is essential for complex calculations where intermediate values improve readability and avoid repeating expressions.

Inline Local Bindings: :with

Creates local variable bindings directly within an expression — typically inside a :map. The first argument defines the variables, the second is the expression that uses them.
{
  ":with": [
    { "pos": { ":sum": ["{@index}", 1] } },
    { "title": "Person {pos}" }
  ]
}

// Inside a :map where @index is 0:
// Result: { "title": "Person 1" }
The difference from :define/:in: :with is designed for use inside loops and transformations where you need a quick local binding without a full :define block. Generating complex dynamic structures:
{
  ":map": [
    { ":range-array": [0, "{data.count}"] },
    {
      ":with": [
        { "pos": { ":sum": ["{@index}", 1] } },
        [
          { "type": "paragraph", "title": "Person {pos}" },
          { "key": "name_{@index}", "type": "text", "title": "Person {pos} name" },
          { "key": "email_{@index}", "type": "email", "title": "Person {pos} email" }
        ]
      ]
    }
  ]
}

// Scope: { "data": { "count": 2 } }
// Result:
// [
//   { "type": "paragraph", "title": "Person 1" },
//   { "key": "name_0", "type": "text", "title": "Person 1 name" },
//   { "key": "email_0", "type": "email", "title": "Person 1 email" },
//   { "type": "paragraph", "title": "Person 2" },
//   { "key": "name_1", "type": "text", "title": "Person 2 name" },
//   { "key": "email_1", "type": "email", "title": "Person 2 email" }
// ]

Raw Expressions: :raw

Captures an expression without evaluating it. The result is the expression structure itself (the AST), not the evaluated value. This is the foundation for building reusable expression templates.
{ ":raw": ["{a}", "{a}"] }

// Scope: { "a": 42 }
// Result: ["{a}", "{a}"]   (not evaluated — still contains variable references)
Defining a reusable template with :define:
{
  ":define": [
    { "exp": { ":raw": ["{value}", "{value}"] } }
  ],
  ":in": "{exp}"
}

// Scope: { "value": 5 }
// Result: ["{value}", "{value}"]   (still raw)
The raw expression is stored for later evaluation with :eval.

Dynamic evaluation: :eval

Evaluates an expression that was previously captured with :raw or built dynamically. This is the counterpart to :raw — it takes a stored expression and runs it in the current scope. Simple evaluation:
{ ":eval": { ":sum": [1, 2, 3] } }
// Result: 6
Combined with :raw for reusable templates:
{
  ":using": {
    "duplicateInArray": { ":raw": ["{a}", "{a}"] }
  },
  ":eval": "{duplicateInArray}"
}

// Scope: { "a": 7 }
//
// Evaluation steps:
// 1. "duplicateInArray" is defined as a raw expression ["{a}", "{a}"]
// 2. :eval resolves "{duplicateInArray}" to that raw expression
// 3. The raw expression is evaluated in a child scope where "a" is 7
//
// Result: [7, 7]
The :using operator defines named expressions — like :define, but specifically designed for storing expression templates that :eval will run.

Pipelines: :pipe

Passes a value through multiple transformation steps sequentially. Each step receives the result of the previous step as @value.
{
  ":pipe": [
    "{data.raw_amount}",
    { ":sum": ["@value", 1] },
    { ":multiply": ["@value", 2] }
  ]
}

// Scope: { "data": { "raw_amount": 10 } }
//
// Flow:
// step 1 → 10
// step 2 → 10 + 1 = 11
// step 3 → 11 × 2 = 22
//
// Result: 22
Pipelines are useful when a value needs to pass through a series of transformations — adding fees, applying tax rates, rounding — where each step depends on the result of the previous one. The @value context variable always contains the output of the most recent step.

Next Steps