Skip to main content
penscript expressions are evaluated in real time. They can be embedded within forms, case templates, automations, notifications, and API calls. Every expression receives a scope — the set of variables and data available at evaluation time — and returns a result. This page is a tour of the language. It introduces the two expression modes, shows how operators compose, and walks through the major categories of what penscript can express.

Two ways to write expressions

JSON Operators

Structured objects where the key defines the operation. Operators start with a colon (:), accept arguments, and return a value.
{ ":sum": [10, 20, 30] }
// → 60
Operators can be nested — the result of one operator becomes the input of another:
{
  ":if": { ":cmp": "{score}", ":gte": 50 },
  ":then": "Passed",
  ":else": "Failed"
}
// Scope: { "score": 72 }
// → "Passed"
Here, :cmp evaluates first (is 72 ≥ 50?), returns true, and :if uses that result to pick the :then branch.

Inline Expressions

Variable references and simple logic embedded inside strings. Lightweight, readable, and ideal for labels, notifications, and short conditions.
"Hello {user.given_name}, your case #{case.reference} is {status}."
// Scope: { "user": { "given_name": "Marie" }, "case": { "reference": "2024-0847" }, "status": "active" }
// → "Hello Marie, your case #2024-0847 is active."
Inline expressions also support pipes, comparisons, and ternaries:
"{data.birthdate | age >= 18 ? 'Adult' : 'Minor'}"
// → "Adult" or "Minor"
Both modes can be mixed freely. A JSON operator can contain inline expressions in its arguments, and inline expressions can reference values computed by operators elsewhere in the configuration.

Composing operators

The real power of penscript comes from composing operators — nesting them to express complex logic in a single, declarative structure. Consider this: you want to display a formatted message that changes based on the number of documents a contact has uploaded.
{
  ":if": { ":cmp": { ":count": "{data.documents}" }, ":eq": 0 },
  ":then": "No documents uploaded yet.",
  ":else": {
    ":if": { ":cmp": { ":count": "{data.documents}" }, ":eq": 1 },
    ":then": "1 document uploaded.",
    ":else": "{ :count: data.documents } documents uploaded."
  }
}
Reading from the inside out: :count counts the documents, :cmp compares the count, and :if picks the right message. Each operator does one thing, and composition builds the behavior. Or, using :case for cleaner multi-branch logic:
{
  ":case": { ":count": "{data.documents}" },
  ":when": [
    [0, "No documents uploaded yet."],
    [1, "1 document uploaded."]
  ],
  ":else": "{ :count: data.documents } documents uploaded."
}

What you can express

Conditions and Branching

penscript handles everything from simple toggles to complex multi-condition logic. Show or hide a form element based on a previous answer:
{
  ":if": "{data.has_vehicle}",
  ":then": { "visible": true, "required": true },
  ":else": { "visible": false }
}
Route a notification based on a contact’s language:
{
  ":case": "{contacts.main.language}",
  ":when": [
    ["fr", "Votre dossier a été mis à jour."],
    ["nl", "Uw dossier is bijgewerkt."],
    ["de", "Ihre Akte wurde aktualisiert."]
  ],
  ":else": "Your case has been updated."
}
Combine multiple conditions to determine eligibility:
{
  ":if": [
    { ":cmp": "{data.birthdate | age}", ":gte": 18 },
    { ":in": ["{data.country}", ["BE", "FR", "LU", "NL"]] },
    { ":not": "{data.has_existing_policy}" }
  ],
  ":then": "Eligible for new policy",
  ":else": "Not eligible"
}
Each condition is a separate operator. The :if evaluates all of them — if they all pass, the :then branch is returned. → Full reference: Logic & comparisons

Calculations and number formatting

Arithmetic, formatting, and number-driven logic. Calculate an insurance premium based on vehicle power:
{
  ":digits": 2,
  ":format-number": {
    ":product": [0.7355, "{data.horses_power}"]
  }
}
// Scope: { "data": { "horses_power": 110 } }
// → "80.91"
Sum multiple amounts:
{ ":sum": ["{data.amount_1}", "{data.amount_2}", "{data.amount_3}"] }
Check whether a total exceeds a threshold:
{
  ":if": {
    ":cmp": { ":sum": ["{data.deductible}", "{data.premium}"] },
    ":gt": 10000
  },
  ":then": "Requires manager approval",
  ":else": "Auto-approved"
}
→ Full reference: Numbers & calculations

Working with collections

Iterate, transform, filter, and query arrays of data. Generate form fields dynamically based on a count the user entered:
{
  ":map": [
    { ":range-array": [0, "{data.number_of_children}"] },
    {
      ":with": [
        { "pos": { ":sum": ["{@index}", 1] } },
        [
          { "type": "paragraph", "title": "Child {pos}" },
          { "key": "child_name_{@index}", "type": "text", "title": "Name of child {pos}", "required": true },
          { "key": "child_birthdate_{@index}", "type": "date", "title": "Date of birth of child {pos}" }
        ]
      ]
    }
  ]
}
This creates a set of form fields for each child — dynamically. If the user enters 3, three sets of fields appear. The {@index} and {@position} variables track where you are in the loop. The :with operator creates a local pos variable (1-based) for display labels. Filter a list to find only completed items:
{
  ":filter": {
    ":map": [
      "{data.tasks}",
      {
        ":if": [
          { ":eq": ["@item.status", "completed"] },
          "@item",
          false
        ]
      }
    ]
  }
}
Check whether any uploaded document is an ID card:
{
  ":some": [
    "{data.documents}",
    { ":eq": ["@item.type", "id_card"] }
  ]
}
// → true if at least one document is tagged as an ID card
Count how many items meet a condition:
{
  ":count": "{data.line_items}",
  ":where": { ":cmp": "@item.amount", ":gt": 100 }
}
// → number of line items above 100
→ Full reference: Loops & arrays

Dates, time, and age

Create dates, format them, compare them, and do arithmetic. Check whether a contract has expired:
{
  ":if": "{{ data.contract_end_date < $today }}",
  ":then": "Contract expired",
  ":else": "Contract active"
}
Calculate a deadline 30 days from now:
{
  ":format-date": {
    ":sum": ["{$today}", "30d"]
  },
  ":pattern": "D/M/Y"
}
// → "13/03/2026"
Determine eligibility based on age:
{
  ":if": { ":cmp": "{data.birthdate | age}", ":gte": 18, ":lte": 65 },
  ":then": "Eligible",
  ":else": "Not eligible"
}
Calculate the number of days between two events:
{
  ":diff": ["{data.start_date}", "{data.end_date}", { ":comparator": "day" }]
}
// → 34 (number of days)
→ Full reference: Dates & time

Internationalization

Serve content in the right language automatically.
{
  ":i18n": {
    "fr": "Bonjour {user.given_name}, veuillez compléter votre dossier.",
    "nl": "Hallo {user.given_name}, gelieve uw dossier te vervolledigen.",
    "en": "Hello {user.given_name}, please complete your case."
  }
}
The :i18n operator reads the active locale from the scope and returns the matching translation. Variables inside translations are resolved normally. → Full reference: Internationalization

Dynamic evaluation

Define local variables, build reusable expression templates, and pipe values through transformation chains. Define intermediate values for a complex calculation:
{
  ":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 } }
// → "Total premium: 97.7"
Each variable can reference the ones defined before it. The :in expression has access to all of them. Pipe a value through multiple steps:
{
  ":pipe": ["{data.raw_amount}", { ":sum": ["@value", 100] }, { ":multiply": ["@value", 1.21] }]
}
// Scope: { "data": { "raw_amount": 500 } }
// Flow: 500 → 600 → 726
// → 726
Capture an expression for reuse:
{
  ":using": {
    "double": { ":raw": { ":multiply": ["{x}", 2] } }
  },
  ":eval": "{double}"
}
// Scope: { "x": 7 }
// → 14
The :raw operator captures the expression without evaluating it. :eval evaluates it later, in a scope where the variables have values. → Full reference: Dynamic evaluation & scope

Real-world composition

These capabilities are designed to be combined. A single penscript configuration might use conditions to control which form elements appear, calculations to compute a premium, loops to generate dynamic fields, date arithmetic to set deadlines, and internationalization to serve it all in the contact’s language. Here’s a realistic fragment from a case automation — automatically moving a case to “Ready for Review” when all required documents are uploaded and the contact is an adult:
{
  ":if": [
    { ":cmp": "{data.birthdate | age}", ":gte": 18 },
    {
      ":every": [
        ["id_card", "proof_of_address", "income_statement"],
        { ":some": ["{data.documents}", { ":eq": ["@item.type", "@item"] }] }
      ]
    }
  ],
  ":then": { "status": "ready_for_review" },
  ":else": { "status": "in_progress" }
}
Reading this: the first condition checks the contact’s age. The second iterates over the list of required document types and, for each one, checks whether any uploaded document matches that type. If both conditions pass, the case moves to “Ready for Review.” This is a single JSON object. No procedural code, no external scripts — just declarative logic that Penbox evaluates in real time.

Where penscript runs

penscript expressions appear throughout the platform:
  • Form templates — element visibility, validation, default values, computed fields, conditional steps
  • Case templates — data schema defaults, status transition conditions, step visibility rules
  • Automations — trigger conditions, status transitions, post-automation payloads
  • Notifications — dynamic content in emails and SMS (subject lines, body text, conditional sections)
  • Document generation — variable injection into Word and PDF templates
  • API payloads — outbound data sent by post-automations to external systems
The visual editor handles configuration for most of these. penscript lets you go further — adding logic, conditions, and dynamic behavior that the visual editor doesn’t expose.

Next Steps