Skip to main content

Formula Authoring Guide

Learn how to write formulas in STELLWERK's visual flow builder. This guide covers everything from basic assignments to advanced collection operations.

Overview

Formulas are the heart of STELLWERK's calculation engine. They let you define business logic, perform calculations, and transform data without writing code. Formulas use a safe, Excel-like syntax powered by the Dentaku expression engine.

What You Can Do

  • Perform arithmetic: price * quantity
  • Apply conditions: IF(total > 100, 0.1, 0)
  • Process arrays: SUM(items[*].price)
  • Chain calculations across multiple nodes

Key Concepts

  • Variables store values you can reference
  • Assignments create new variables
  • Exports pass values to downstream nodes
  • Functions perform operations on data

Where Formulas Are Used

Calculate Main formula logic with assignments
Condition Boolean expressions for branching
Switch Case matching expressions

Your First Formula

Tutorial

Let's build a simple pricing formula step by step. We'll calculate a total price with a discount.

1

Start with Inputs

Assume your StartNode defines two inputs: quantity and unit_price.

# These come from your StartNode inputs
quantity = 5
unit_price = 29.99
2

Calculate the Subtotal

In a CalculateNode, multiply quantity by price:

subtotal = quantity * unit_price

Result: subtotal = 149.95

3

Apply a Conditional Discount

Use IF() to apply 10% discount for orders over $100:

discount_rate = IF(subtotal > 100, 0.10, 0)
discount = subtotal * discount_rate
total = subtotal - discount

Result: discount = 14.995, total = 134.955

4

All Variables Flow Downstream

All assigned variables are automatically available to the next node. No export needed!

subtotal = quantity * unit_price
discount_rate = IF(subtotal > 100, 0.10, 0)
discount = subtotal * discount_rate
total = subtotal - discount

# All four variables (subtotal, discount_rate, discount, total)
# are automatically available in the next node

Complete Formula

# Calculate order total with volume discount
subtotal = quantity * unit_price
discount_rate = IF(subtotal > 100, 0.10, 0)
discount = subtotal * discount_rate
total = subtotal - discount

Input: quantity=5, unit_price=29.99
Output: subtotal=149.95, discount_rate=0.10, discount=14.995, total=134.955

Assignments

Core

Assignments create new variables by computing expressions. Each assignment stores a value that can be used later in the same formula or exported to downstream nodes.

Syntax

variable_name = expression

Rules:

  • Variable names must start with a letter or underscore
  • Can contain letters, numbers, and underscores: my_var_2
  • One assignment per line
  • Use snake_case for readability

Examples

Simple arithmetic:

total = price * quantity
tax = total * 0.19
grand_total = total + tax

Using functions:

max_price = MAX(price_a, price_b, price_c)
rounded = ROUND(total, 2)
absolute = ABS(difference)

Conditional assignment:

discount = IF(is_member, 0.15, 0.05)
status = IF(quantity > 0, "in_stock", "out_of_stock")

String values:

message = "Order confirmed"
error_code = 'ERR_NOT_FOUND'

Comments

Use # to add comments. Comments are ignored during execution.

# Calculate the base price
base = quantity * unit_price

# Apply regional tax rate (19% for DE)
tax = base * 0.19

# Final price including tax
total = base + tax

Available Operators

Arithmetic:

+ Addition
- Subtraction
* Multiplication
/ Division
% Modulo (remainder)
^ Exponentiation

Comparison:

== Equal
!= Not equal
> Greater than
< Less than
>= Greater or equal
<= Less or equal

Variable Availability

By default, all variables you assign in a CalculateNode are automatically available to downstream nodes in the same branch. You don't need to explicitly export them.

Default behavior: All assigned variables automatically flow to the next node. No export statement needed!

How Variables Flow

1

Default: All variables available

Every variable you assign is automatically passed downstream

subtotal = quantity * price
tax = subtotal * 0.19
total = subtotal + tax
# All three (subtotal, tax, total) are available downstream
2

Optional: Limit with export

Use export only if you want to restrict which variables are passed

_temp = quantity * price
_adjusted = _temp * 0.95
total = _adjusted + fees

export total
# Only 'total' is available downstream
3

Alternative: return for single value

Shorthand when you only want one variable downstream

base = hours * rate
overtime = MAX(hours - 40, 0) * rate * 0.5
gross_pay = base + overtime

return gross_pay

When to Use export

Use export or return when you want to:

  • Hide internal variables: Prevent temporary calculations from cluttering the context
  • Be explicit: Document exactly which values are important outputs
  • Avoid naming conflicts: Prevent intermediate variables from overwriting upstream values

Examples

Default All variables automatically available
# No export needed - all three flow downstream
subtotal = quantity * unit_price
tax = subtotal * 0.19
total = subtotal + tax

# Next node can use: subtotal, tax, total
Restricted Limit what flows downstream
# Only pass the final total, hide internals
_base = quantity * unit_price
_adjusted = _base * discount_factor
total = _adjusted + shipping

export total

# Next node can only use: total
Shorthand Single value with return
# return is shorthand for export when you have one output
base = hours * hourly_rate
overtime = MAX(hours - 40, 0) * hourly_rate * 0.5
gross_pay = base + overtime

return gross_pay

Export Syntax

When using export, separate variable names with commas or spaces:

export subtotal, tax, total
export subtotal tax total   # also valid

Conditions

Branching

Conditions are boolean expressions that evaluate to true or false. They're used in ConditionNodes for branching and in SwitchNodes for case matching.

Condition Node

Splits flow into "true" and "false" branches based on a single condition.

total > 100 AND is_member

Switch Node

Routes to different branches based on multiple case conditions.

region == "EU"
region == "US"
# default case

Logical Operators

Operator Meaning Example
AND Both must be true age >= 18 AND has_license
OR At least one must be true is_admin OR is_owner
NOT Inverts the result NOT is_blocked

Condition Examples

Simple comparisons:

# Numeric comparison
quantity > 10

# String comparison
status == "active"

# Not equal
country != "US"

Combined conditions:

# Both must be true
total > 100 AND customer_type == "premium"

# Either can be true
is_sale OR has_coupon

# Complex logic with parentheses
(age >= 18 AND age <= 65) OR has_guardian_consent

Real-world examples:

# Free shipping eligibility
(subtotal >= 50 AND region == "domestic") OR is_prime_member

# Discount qualification
(order_count > 5 OR total_spent > 500) AND NOT is_first_order

# Business hours check
hour >= 9 AND hour < 17 AND day != "Sunday"

IF Function in Formulas

Use IF(condition, value_if_true, value_if_false) for conditional values within CalculateNodes:

# Simple conditional
discount = IF(is_member, 0.15, 0.05)

# Tiered pricing
rate = IF(quantity > 100, 0.80, IF(quantity > 50, 0.90, 1.0))

# Status assignment
status = IF(score >= 70, "pass", "fail")

# Nested conditions
shipping = IF(is_local, 0, IF(weight > 10, 15.99, 5.99))

Important: No assignments in conditions

Conditions must be pure expressions. You cannot use = for assignment in a ConditionNode or SwitchNode case.

# Wrong - this is an assignment, not a comparison
x = 5

# Correct - this is a comparison
x == 5

Variables & Scope

Variables are the data that flows through your nodes. Understanding where variables come from and where they can be used is key to building reliable flows.

Where Variables Come From

1

Runtime Parameters (API Inputs)

Values passed when executing the flow via API

# From API call: { "quantity": 5, "price": 29.99 }
total = quantity * price
2

StartNode Defaults

Default values defined in your StartNode input schema

# StartNode defines: tax_rate with default 0.19
tax = subtotal * tax_rate  # uses 0.19 if not provided
3

Flow Metadata

Key-value pairs defined on the Flow itself

# Flow metadata: { "version": "2.0", "region": "EU" }
shipping = IF(region == "EU", 5.99, 9.99)
4

From Upstream Nodes

All variables from nodes earlier in the flow

# Previous node calculated 'subtotal' and 'discount'
final = subtotal - discount

Variable Priority (highest to lowest)

  1. Runtime parameters (API inputs) - always win
  2. Exported variables from upstream nodes
  3. Flow metadata
  4. StartNode defaults

Understanding Scope

All assigned variables flow downstream:

# Node A
temp = price * 1.1
result = temp + fees

# Node B (downstream) - can use BOTH variables
x = result              # ✓ works
y = temp                # ✓ works - all variables available!

Variables flow forward, not backward:

# Flow: StartNode → NodeA → NodeB → EndNode

# NodeA can use:
#   - Runtime inputs
#   - StartNode defaults
#   - Flow metadata

# NodeB can use:
#   - Everything NodeA can use
#   - Plus: all variables from NodeA

Naming Conventions

Recommended:

  • snake_case
  • unit_price
  • _internal_temp (prefix with _ for internal use)

Avoid:

  • camelCase
  • 123var (can't start with number)
  • my-var (no hyphens)

Note: Variables are case-sensitive. total and Total are different variables.

Accessing Nested Data

Use dot notation to access nested object properties:

# Given input: { "order": { "total": 150, "items": [...] } }

amount = order.total
tax = order.total * 0.19

# Works with deeper nesting too
city = customer.address.city

Aggregation Functions

Numbers

Aggregation functions compute a single value from multiple inputs. They work with both arrays and multiple arguments.

SUM

Returns: Number

Adds up all numeric values. Non-numeric values are ignored.

# With multiple arguments
total = SUM(10, 20, 30)                    # → 60

# With an array
total = SUM(prices)                        # → sum of all prices

# With projection (array of objects)
total = SUM(items[*].price)                # → sum of all item prices

# Mixed types (strings converted if numeric-looking)
total = SUM(["5", 2, "3.5"])               # → 10.5

# Empty array
total = SUM([])                            # → 0

COUNT

Returns: Integer

Counts non-nil values. Zeros and empty strings are counted; nil/null values are not.

# Count array elements
num_items = COUNT(items)                   # → number of items

# Count with projection
num_prices = COUNT(items[*].price)         # → count of price values

# Nil values excluded
count = COUNT([1, nil, 3, 0])              # → 3 (nil excluded, 0 counts)

# Multiple arguments
count = COUNT(a, b, c)                     # → counts non-nil args

MIN / MAX

Returns: Number or nil

Find the minimum or maximum numeric value. Returns nil if no numeric values found.

# Find extremes
lowest = MIN(prices)                       # → smallest price
highest = MAX(prices)                      # → largest price

# With projection
min_price = MIN(items[*].price)            # → cheapest item
max_price = MAX(items[*].price)            # → most expensive item

# Multiple arguments
biggest = MAX(a, b, c)                     # → largest of a, b, c

# Numeric string conversion
smallest = MIN(["5", 2, "3.5"])            # → 2

# Empty array
result = MIN([])                           # → nil

AVERAGE / AVG

Returns: Number or nil

Calculates the arithmetic mean of numeric values. Returns nil if no numeric values found.

# Average of array
avg_price = AVERAGE(prices)                # → mean of all prices
avg_price = AVG(prices)                    # → same (alias)

# With projection
avg_score = AVERAGE(reviews[*].rating)     # → average rating

# Multiple arguments
mean = AVERAGE(10, 20, 30)                 # → 20

# Empty array
result = AVERAGE([])                       # → nil

Understanding Projection Syntax

The [*] syntax extracts a field from each element in an array:

# Given: items = [{ price: 10 }, { price: 20 }, { price: 30 }]

items[*].price    # → [10, 20, 30]
SUM(items[*].price)   # → 60

# Nested objects work too:
orders[*].customer.name   # → extracts name from each order's customer

Collection Functions

Arrays

Collection functions help you work with arrays - selecting elements, removing duplicates, and accessing specific items.

FIRST / LAST

Returns: Element or nil

Get the first or last element of an array. Returns nil for empty arrays.

# Get first/last element
first_item = FIRST(items)                  # → first element
last_item = LAST(items)                    # → last element

# With projection
first_price = FIRST(items[*].price)        # → price of first item
last_name = LAST(users[*].name)            # → name of last user

# Single element (scalar) auto-wrapped
result = FIRST(42)                         # → 42

# Empty array
result = FIRST([])                         # → nil

TAKE

Returns: Array

Get the first N elements from an array. Useful for limiting results.

# Get first N elements
top_three = TAKE(items, 3)                 # → first 3 items

# With projection
top_prices = TAKE(items[*].price, 5)       # → first 5 prices

# Negative or zero returns empty
result = TAKE(items, 0)                    # → []
result = TAKE(items, -2)                   # → []

# More than available returns all
result = TAKE([1, 2, 3], 10)               # → [1, 2, 3]

DISTINCT

Returns: Array

Remove duplicate values from an array. Preserves order (keeps first occurrence).

# Remove duplicates
unique = DISTINCT([1, 2, 2, 3, 1])         # → [1, 2, 3]

# With strings
unique_names = DISTINCT(["a", "b", "a"])   # → ["a", "b"]

# With projection - unique categories
categories = DISTINCT(products[*].category)

# Count unique values
unique_count = COUNT(DISTINCT(items[*].type))

PROJECT

Returns: Array

Extract a field from each element in an array. This is what [*] syntax uses internally.

# These are equivalent:
prices = items[*].price
prices = PROJECT(items, "price")

# Missing fields become nil
# [{ price: 10 }, { name: "x" }] → [10, nil]

Practical Examples

# Get unique countries from orders
countries = DISTINCT(orders[*].shipping_country)

# Find price range
price_range = MAX(items[*].price) - MIN(items[*].price)

# Get top 3 most recent items (assuming sorted)
recent = TAKE(items, 3)

# Check if first item is expensive
first_is_premium = FIRST(items[*].price) > 100

Transformation Functions

Advanced

Transformation functions let you modify each element in an array, filter elements based on conditions, or accumulate values with custom logic.

MAP

Returns: Array

Transform each element using an expression. Use item to reference the current element.

# Double each value
doubled = MAP([1, 2, 3], "item * 2")       # → [2, 4, 6]

# Apply discount to prices
discounted = MAP(prices, "item * 0.9")    # → 10% off each

# Access object fields via 'item' or directly
totals = MAP(items, "item.price * item.quantity")
totals = MAP(items, "price * quantity")   # same result

# Calculate line totals with tax
with_tax = MAP(items, "price * quantity * 1.19")

Tip: When mapping over objects, each key is available as a variable. So for {price: 10, qty: 2}, you can use price * qty directly.

FILTER

Returns: Array

Keep only elements where the predicate is true. Use item to reference each element.

# Keep values over 10
large = FILTER([5, 15, 8, 20], "item > 10")    # → [15, 20]

# Filter by object property
expensive = FILTER(items, "price > 50")

# Filter by category
electronics = FILTER(products, "category == 'electronics'")

# Multiple conditions
premium = FILTER(items, "price > 100 AND in_stock")

# Combine with aggregation
expensive_total = SUM(FILTER(items, "price > 50")[*].price)

REDUCE

Returns: Any

Accumulate values with custom logic. Use acc (or accumulator) for the running total and item for each element.

# Sum (same as SUM function)
total = REDUCE([1, 2, 3], 0, "acc + item")     # → 6

# Product of all values
product = REDUCE([2, 3, 4], 1, "acc * item")   # → 24

# Find running maximum
max_val = REDUCE(values, 0, "IF(item > acc, item, acc)")

# Sum with weighted values
weighted = REDUCE(items, 0, "acc + (price * weight)")

Note: REDUCE is powerful but complex. For simple sums, counts, min/max, prefer the dedicated functions which are more readable.

TRACE

Returns: Same value

Pass-through function for debugging. Returns the value unchanged. (Reserved for future instrumentation.)

# Debug intermediate value
result = TRACE(complex_calculation, "step1")

# Currently just returns the first argument unchanged

Chaining Transformations

Combine functions for powerful data processing:

# Get total of expensive items only
expensive_items = FILTER(items, "price > 50")
expensive_total = SUM(expensive_items[*].price)

# Apply 20% discount to all items, then sum
discounted_prices = MAP(items, "price * 0.8")
discounted_total = SUM(discounted_prices)

# Count unique categories of in-stock items
in_stock = FILTER(products, "stock > 0")
unique_categories = COUNT(DISTINCT(in_stock[*].category))

Conditional Functions

Logic

Conditional functions combine aggregation with filtering, letting you sum or count only elements that match a condition.

SUMIF

Returns: Number

Sum values where a condition is true. Optionally project a field to sum.

# Sum prices of expensive items
expensive_total = SUMIF(items, "price > 50", "price")

# Sum values directly (no projection needed for simple arrays)
positive_sum = SUMIF([5, -3, 10, -2], "item > 0")  # → 15

# Sum by category
electronics_total = SUMIF(products, "category == 'electronics'", "price")

# Sum with compound condition
premium_sales = SUMIF(orders, "total > 100 AND status == 'completed'", "total")

# Real-world: Sum line totals for taxable items
taxable_amount = SUMIF(line_items, "is_taxable", "price * quantity")

COUNTIF

Returns: Integer

Count elements where a condition is true.

# Count expensive items
expensive_count = COUNTIF(items, "price > 50")

# Count positive values
positive_count = COUNTIF([5, -3, 10, -2], "item > 0")  # → 2

# Count by status
active_users = COUNTIF(users, "status == 'active'")
pending_orders = COUNTIF(orders, "status == 'pending'")

# Count with compound condition
vip_count = COUNTIF(customers, "total_spent > 1000 AND is_subscribed")

# Check if any match (count > 0)
has_errors = COUNTIF(items, "has_error") > 0

SUMIF/COUNTIF vs FILTER

These are equivalent approaches - use whichever is more readable:

# Using SUMIF (more concise)
total = SUMIF(items, "price > 50", "price")

# Using FILTER + SUM (more explicit)
expensive = FILTER(items, "price > 50")
total = SUM(expensive[*].price)

# Using COUNTIF
count = COUNTIF(items, "in_stock")

# Using FILTER + COUNT
count = COUNT(FILTER(items, "in_stock"))

Real-World Calculations

# E-commerce order summary
subtotal = SUM(items[*].price)
taxable_amount = SUMIF(items, "is_taxable", "price")
tax = taxable_amount * 0.19
discounted_items = COUNTIF(items, "has_discount")

# Inventory analysis
low_stock_count = COUNTIF(products, "stock < 10")
out_of_stock_value = SUMIF(products, "stock == 0", "price * reorder_qty")

# Sales report
completed_revenue = SUMIF(orders, "status == 'completed'", "total")
pending_count = COUNTIF(orders, "status == 'pending'")
refunded_amount = SUMIF(orders, "status == 'refunded'", "total")

Error Messages

Troubleshooting

When something goes wrong, the formula validator provides helpful error messages. Here's how to understand and fix common issues.

Error Cause Fix
Unknown: var1, var2 Referenced variables don't exist in scope Check spelling, ensure variable is defined in an upstream node, or define it in StartNode
Assignment not allowed in condition Used = for assignment in a ConditionNode Use == for comparison, or move assignment to a CalculateNode
Right-hand side required Assignment has no value after = Add an expression: x = 10
Unbalanced parentheses Opening and closing parentheses don't match Count ( and ) - they should be equal
Required input missing: X StartNode requires input X but it wasn't provided Provide the input in API call, or add a default value in StartNode
MapNode source 'X' is not an array MapNode expects an array but got a different type Ensure the source variable is an array in your input or upstream calculation

Common Mistakes

Wrong: Using = instead of == in conditions

# In a ConditionNode - WRONG
status = "active"

# Correct
status == "active"

Wrong: Using undefined variable

# 'discount_rate' was never defined - WRONG
final = subtotal * (1 - discount_rate)

# Correct: define it first or pass as input
discount_rate = 0.1
final = subtotal * (1 - discount_rate)

Wrong: Using export with undefined variable

# Trying to export a variable that doesn't exist - WRONG
total = price * quantity
export total, discount  # ERROR: 'discount' was never defined

# Fix: only export variables you've defined
discount = IF(quantity > 10, 0.1, 0)
total = price * quantity * (1 - discount)
export total, discount  # ✓ both variables exist

Wrong: Case sensitivity mismatch

# Input is 'userId' but you reference 'userid' - WRONG
result = userid * 2   # Unknown: userid

# Correct: match exact case
result = userId * 2

Debugging Tips

  • Check the context in the API response to see all available variables
  • Use applied_nodes in the response to see which nodes executed
  • Start simple: test formulas with hardcoded values before using variables
  • Use comments to document expected values: # expects price > 0
  • Create test cases in the flow to validate expected inputs/outputs

Best Practices

Guidelines

Follow these guidelines to write maintainable, readable, and reliable formulas.

1 Use Clear, Descriptive Names

Good

order_subtotal = quantity * unit_price
shipping_cost = IF(order_subtotal > 50, 0, 5.99)
tax_amount = order_subtotal * tax_rate

Avoid

x = q * p
s = IF(x > 50, 0, 5.99)
t = x * r

2 Keep Formulas Focused

Each CalculateNode should do one thing well. Split complex logic across multiple nodes.

Better: Split into nodes

# Node 1: Calculate subtotal
subtotal = quantity * unit_price
# subtotal flows automatically to next node

# Node 2: Apply discounts
discount = IF(subtotal > 100, 0.1, 0)
discounted = subtotal * (1 - discount)
# discount, discounted flow automatically

# Node 3: Add tax and shipping
tax = discounted * 0.19
shipping = IF(discounted > 50, 0, 5.99)
total = discounted + tax + shipping

Harder to maintain: All in one

subtotal = quantity * unit_price
discount = IF(subtotal > 100, 0.1, 0)
discounted = subtotal * (1 - discount)
tax = discounted * 0.19
shipping = IF(discounted > 50, 0, 5.99)
total = discounted + tax + shipping
# All 6 variables flow downstream automatically

3 Use Export to Limit Variables

By default, all assigned variables flow downstream. Use export only when you want to limit which variables are passed.

# Without export: gross, tax, and net ALL flow downstream
gross = hours * rate
tax = gross * 0.25
net = gross - tax

# With export: ONLY net flows downstream (hides internals)
export net

4 Document Business Logic

Use comments to explain the "why", especially for business rules that aren't obvious.

# Loyalty discount: 15% for customers with 5+ orders
# As per marketing policy Q4-2025
loyalty_discount = IF(order_count >= 5, 0.15, 0)

# Free shipping threshold is $50 domestic, $100 international
free_shipping_threshold = IF(is_domestic, 50, 100)
shipping = IF(subtotal >= free_shipping_threshold, 0, base_shipping)

5 Prefix Internal Variables

Use underscore prefix as a convention to indicate internal/temporary calculations. Combine with export to hide them from downstream nodes.

# Internal calculations (prefixed with _ by convention)
_base_score = points / max_points * 100
_with_bonus = _base_score + bonus_points
_capped = MIN(_with_bonus, 100)

# Final result (no prefix - the main output)
final_score = ROUND(_capped, 0)

# Use export to only pass final_score downstream
export final_score

6 Create Test Cases

Use the built-in test case feature to validate your flow with known inputs and expected outputs.

  • Test edge cases: zero values, empty arrays, boundary conditions
  • Test each conditional branch to ensure all paths work
  • Document expected behavior with descriptive test case names

Frequently Asked Questions

Q: Do I need to use export?

A: No! By default, all assigned variables automatically flow to downstream nodes. Only use export or return if you want to limit which variables are passed.

Q: Are variable names case-sensitive?

A: Yes. total, Total, and TOTAL are three different variables. Stick to snake_case for consistency.

Q: Can I export a variable I didn't assign?

A: No. Exporting an unknown variable will cause an error. You can only export variables that exist in scope (defined in the current node or available from upstream).

Q: Are there built-in default variables like price or metadata?

A: No. There are no magic default variables. All variables come from:

  • Runtime parameters (API inputs)
  • StartNode defaults
  • Flow metadata
  • Exported from upstream nodes

Q: What happens if I divide by zero?

A: Division by zero will cause an execution error. Protect against it with a condition:

result = IF(divisor != 0, value / divisor, 0)

Q: How do I handle null/nil values?

A: Use conditional logic or provide defaults:

# Using IF to provide default
safe_value = IF(value, value, 0)

# Or use StartNode defaults for inputs

Q: Can I call one flow from another?

A: Yes, using MapNodes. A MapNode can execute a sub-flow for each element in an array. This is useful for processing collections with complex logic.

Q: Why does my function show "unknown variable" warning?

A: Check these common issues:

  • Spelling: SUM not Sum or sum
  • Parentheses: SUM(items) not SUM items
  • Recognized functions: see the Functions sections for the complete list

Q: How do I pass arrays from the API?

A: Pass arrays directly in your JSON input:

{
  "inputs": {
    "items": [
      { "name": "Widget", "price": 10 },
      { "name": "Gadget", "price": 25 }
    ]
  }
}

Then use projection: SUM(items[*].price)

Q: Is the formula language Turing-complete?

A: No, intentionally. Formulas are designed for safe, predictable calculations. There are no loops or recursion to prevent infinite execution. Use multiple nodes for complex logic.