> ## Documentation Index
> Fetch the complete documentation index at: https://docs.penbox.io/llms.txt
> Use this file to discover all available pages before exploring further.

# How penscript works

> Real-time expressions that power dynamic behavior across Penbox

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.

```json theme={null}
{ ":sum": [10, 20, 30] }
// → 60
```

Operators can be nested — the result of one operator becomes the input of another:

```json theme={null}
{
  ":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.

```json theme={null}
"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:

```json theme={null}
"{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.

```json theme={null}
{
  ":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:

```json theme={null}
{
  ":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:**

```json theme={null}
{
  ":if": "{data.has_vehicle}",
  ":then": { "visible": true, "required": true },
  ":else": { "visible": false }
}
```

**Route a notification based on a contact's language:**

```json theme={null}
{
  ":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:**

```json theme={null}
{
  ":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](/penscript/logic)

### Calculations and number formatting

Arithmetic, formatting, and number-driven logic.

**Calculate an insurance premium based on vehicle power:**

```json theme={null}
{
  ":digits": 2,
  ":format-number": {
    ":product": [0.7355, "{data.horses_power}"]
  }
}
// Scope: { "data": { "horses_power": 110 } }
// → "80.91"
```

**Sum multiple amounts:**

```json theme={null}
{ ":sum": ["{data.amount_1}", "{data.amount_2}", "{data.amount_3}"] }
```

**Check whether a total exceeds a threshold:**

```json theme={null}
{
  ":if": {
    ":cmp": { ":sum": ["{data.deductible}", "{data.premium}"] },
    ":gt": 10000
  },
  ":then": "Requires manager approval",
  ":else": "Auto-approved"
}
```

→ Full reference: [Numbers & calculations](/penscript/numbers)

### Working with collections

Iterate, transform, filter, and query arrays of data.

**Generate form fields dynamically based on a count the user entered:**

```json theme={null}
{
  ":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:**

```json theme={null}
{
  ":filter": {
    ":map": [
      "{data.tasks}",
      {
        ":if": [
          { ":eq": ["@item.status", "completed"] },
          "@item",
          false
        ]
      }
    ]
  }
}
```

**Check whether any uploaded document is an ID card:**

```json theme={null}
{
  ":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:**

```json theme={null}
{
  ":count": "{data.line_items}",
  ":where": { ":cmp": "@item.amount", ":gt": 100 }
}
// → number of line items above 100
```

→ Full reference: [Loops & arrays](/penscript/arrays)

### Dates, time, and age

Create dates, format them, compare them, and do arithmetic.

**Check whether a contract has expired:**

```json theme={null}
{
  ":if": "{{ data.contract_end_date < $today }}",
  ":then": "Contract expired",
  ":else": "Contract active"
}
```

**Calculate a deadline 30 days from now:**

```json theme={null}
{
  ":format-date": {
    ":sum": ["{$today}", "30d"]
  },
  ":pattern": "D/M/Y"
}
// → "13/03/2026"
```

**Determine eligibility based on age:**

```json theme={null}
{
  ":if": { ":cmp": "{data.birthdate | age}", ":gte": 18, ":lte": 65 },
  ":then": "Eligible",
  ":else": "Not eligible"
}
```

**Calculate the number of days between two events:**

```json theme={null}
{
  ":diff": ["{data.start_date}", "{data.end_date}", { ":comparator": "day" }]
}
// → 34 (number of days)
```

→ Full reference: [Dates & time](/penscript/dates)

### Internationalization

Serve content in the right language automatically.

```json theme={null}
{
  ":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](/penscript/internationalization)

### Dynamic evaluation

Define local variables, build reusable expression templates, and pipe values through transformation chains.

**Define intermediate values for a complex calculation:**

```json theme={null}
{
  ":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:**

```json theme={null}
{
  ":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:**

```json theme={null}
{
  ":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](/penscript/dynamic_evaluation)

## 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:

```json theme={null}
{
  ":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

<CardGroup cols={2}>
  <Card title="Variables" icon="brackets-curly" href="/penscript/variables">
    Learn about variable syntax and scope
  </Card>

  <Card title="Logic & comparisons" icon="code-branch" href="/penscript/logic">
    Conditions, branching, and boolean logic
  </Card>

  <Card title="Numbers & calculations" icon="calculator" href="/penscript/numbers">
    Arithmetic and number formatting
  </Card>

  <Card title="Loops & arrays" icon="repeat" href="/penscript/arrays">
    Working with collections
  </Card>
</CardGroup>
