Card Creator — Calculated Columns Guide

What are Calculated Columns?

A calculated column is a new column you add to a card that computes its value from other columns in the same card. Think of it like writing a formula in Excel — but it lives inside Tenzo and updates automatically.

Card Creator — Add Calculated Column
Column Name
Labour %
Formula
round([Labour Cost] / [Net Sales] * 100, 1)
Data Format
Percent Color
Apply to View (show in grouped rows)
📌 How to open it In any card, click Edit → scroll down to Calculated Columns → click Add Column. You can add as many as you need.
Filters on Cards

Filters control what data your card shows. There are two distinct types — interactive filters that users can change on the fly, and default filters that are baked into the card and invisible to end users.

The two types of filter

Interactive

Regular Filters

Appear as a visible dropdown on the card. Users can select and change values. Common examples: Date, Location, Revenue Centre, Day of Week.

Permanent

Default Filters

Permanently scope the card's data. They are hidden from end users — no dropdown appears. Use when you want a card to always show one category or location only.

Adding a Default Filter

1
In Card Creator, click the settings cog ⚙️ next to the metric or dimension you want to filter.
2
The field must already be present as a Filter, Row, Column, or Value in the card. If you don't want it visible on the card itself, you can toggle it to hidden.
3
Click "+ Add default filter" and choose your condition and value.
⚠️ Default filter trade-off When you add a default filter to a dimension (e.g. Location), you lose the ability to have a user-facing dropdown for that dimension. If users regularly need to swap between values, use a regular interactive filter instead.

Filter conditions

ConditionWhat it doesExample use case
InExact match — shows only the specified value(s)Show only "Shoreditch" location
Not InExact match — excludes the specified value(s)Exclude "Test Location" from all data
ContainsPartial match — includes any value with this textShow all locations with "Kiosk" in the name
Does Not ContainPartial match — excludes any value with this textExclude all kiosks from the report
regex_containsRegex pattern — use | for OR matchingMatch "eat in|dine in|restaurant" in one go
⚠️ Filter values are case-sensitive "Shoreditch" and "shoreditch" are treated as different values. Always match the exact spelling and capitalisation from your data. Use Contains (case-insensitive) if you're unsure of the exact casing.

Using regex_contains in filters

You can type a regex_contains pattern directly into a Revenue Centre filter field. Unlike in formulas, you do not wrap the pattern in quotes — just paste it directly:

// In a filter field — no quotes needed:
eat in|dine in|restaurant|table service

// In a calculated column formula — quotes required:
regex_contains([Revenue Centre], "eat in|dine in|restaurant|table service")

Dashboard filters vs Card filters

Cards live on dashboards, and dashboards can have their own Date, Location, and Area filters. Here's how they interact:

ScenarioWhat happens
Card has no location filter; dashboard tab applies a location Card opens with the dashboard's location pre-selected. User can change it within the card view.
Card has a default location filter; dashboard applies the same location Card loads filtered to that location. The location filter is not visible (it's the default).
Card has a default location filter; dashboard applies a different location The two filters conflict — card returns no data. The filter button is hidden.
Card has no available filters at all The filter button is hidden entirely on that card.
📌 Standard filters automatically added to cards All cards should have Date, Location, and Area filters available unless a default filter has already locked one of those dimensions. Date always shows; Location and Area only appear for users with access to multiple locations or areas.

Practical examples

Default Filter — show data for one category only
Dimension: Category
Category
Condition
In
Value
Chicken
Result: card permanently shows Chicken data only. No category dropdown visible to end users.
Default Filter — exclude a test location
Dimension: Location
Location
Condition
Does Not Contain
Value
Test
Result: any location with "Test" in its name is permanently hidden from this card.
💡 When to use filters vs calculated columns for grouping Use a default filter when you want to exclude data permanently (e.g. remove test locations, scope to one category). Use a calculated column with if() or regex_contains() when you want to re-label or group existing data (e.g. combine "Eat In" and "Dine In" into one category). Both can work together on the same card.
Key Settings

These settings control how your calculated column behaves and displays. Getting them right makes a big difference.

Display

Data Format

Controls the visual style of values. Use Percent Color for %, Currency Color for £/$, Left Justified for text labels.

Totals

Total Row

Set to Sum for additive values. Set to Calculate for ratios and percentages — otherwise the total will be wrong.

Totals

Row Type

Controls how subtotals are computed: SUM adds values, MIN shows the minimum. Use MIN for rate columns like wage rates.

Grouping

Apply to View

Must be ON if you want the column to appear when rows are grouped (e.g. by Department or Site). Required for renaming/grouping use cases.

⚠️ Percentage columns and Total Row If your formula produces a percentage (e.g. Labour / Sales × 100), always set Total Row → Calculate. If you leave it on Sum, the total row will add up all the percentages instead of recalculating — giving a meaningless result.
Simple Maths

The simplest calculated columns just add, subtract, multiply or divide existing columns. Column names are always wrapped in square brackets.

Example: Gross Profit

Formula
[Net Sales] - [Cost of Goods]
DateNet SalesCost of GoodsGross Profit ✦
Mon£4,200£1,400£2,800
Tue£3,800£1,250£2,550
Wed£5,100£1,700£3,400
Total£13,100£4,350£8,750

Example: Revenue Per Cover

Formula
[Net Sales] / [Covers]
💡 Tip: Use round() to keep decimals tidy Wrap any division in round([Net Sales] / [Covers], 2) to limit to 2 decimal places.
Percentages & Ratios

Percentage columns are one of the most useful things you can build. Just remember to set Total Row to Calculate or the total will be wrong.

Example: Labour % of Sales

Formula
round([Labour Cost] / [Net Sales] * 100, 1)
SiteLabour CostNet SalesLabour % ✦
Shoreditch£6,200£22,00028.2%
Soho£7,800£24,50031.8%
Canary Wharf£5,400£19,80027.3%
Total£19,400£66,30029.3%
⚠️ Settings checklist for this example Data Format → Percent Color  |  Total Row → Calculate  |  Row Type → SUM

Example: Sales Mix %

Formula
round([Category Sales] / [Total Sales] * 100, 1)
Conditional Logic — if()

Use if() to return different values depending on a condition. You can nest up to 6 if() statements for more complex logic.

Syntax

if(condition, value_if_true, value_if_false)

Example: Flag high labour %

Formula
if([Labour %] > 35, "⚠️ High", "✓ OK")
SiteLabour %Status ✦
Shoreditch28.2%✓ OK
Soho31.8%✓ OK
Canary Wharf38.5%⚠️ High

Example: Tiered performance band (nested if)

Formula — up to 6 levels of nesting supported
if([Labour %] < 25, "🟢 Excellent",
  if([Labour %] < 30, "🟡 Good",
    if([Labour %] < 35, "🟠 Watch", "🔴 Over")))
💡 Remember: strings go in "double quotes" Any text value in a formula must be wrapped in double quotes. Column references always use [square brackets]. Numbers are written as-is.
Grouping & Renaming with if() / contains()

Use calculated columns to map long or inconsistent dimension names into cleaner categories. This is especially powerful for Revenue Centres that vary by site.

Example: Simplify Revenue Centre names

Formula
if(icontains([Revenue Centre], "delivery"), "Delivery",
  if(icontains([Revenue Centre], "takeaway"), "Takeaway",
    if(icontains([Revenue Centre], "eat in"), "Eat In", "Other")))
Original Revenue CentreGrouped Category ✦
Deliveroo - Chicken Shop (London)Delivery
UberEats - Takeaway CounterTakeaway
Restaurant Eat In - MainEat In
Bar RevenueOther
⚠️ Apply to View must be ON If you want to use this grouped column in the Rows section (to group by it), you must enable Apply to View in the column settings. Without it, the grouping won't work.

Available text matching functions

FunctionBehaviourExample
contains(col, "text")Case-sensitive match anywhere in stringMatches "Delivery" not "delivery"
icontains(col, "text")Case-insensitive match anywhereMatches "Delivery" and "delivery"
startswith(col, "text")Case-sensitive, matches startMatches "Uber..." not "My Uber..."
istartswith(col, "text")Case-insensitive, matches startMatches "uber..." and "Uber..."
regex_contains(col, pattern)Full regex, pipe | for ORSee Regex section below
Regex Filtering — regex_contains()

If you have dozens of Revenue Centres with complex naming, regex_contains() with the pipe | operator is your most powerful tool. It lets you match multiple patterns in one expression.

🎥 Real-world use case Tenzo customers often have 60+ Revenue Centre variations across locations and brands (e.g. "Chicken Shop Shoreditch - Eat In", "Chicken Shop Soho - Eat In", etc.). Instead of writing a separate if(icontains(...)) for each one, you can use regex with | to match all eat-in centres in a single pattern — reducing 65 conditions down to just 8.

Syntax: using | for OR

Formula — match any of several patterns
if(regex_contains([Revenue Centre], "eat in|dine in|restaurant|table service"), "Eat In",
  if(regex_contains([Revenue Centre], "takeaway|take away|click & collect"), "Takeaway",
    if(regex_contains([Revenue Centre], "deliveroo|uber eats|just eat|delivery"), "Delivery", "Other")))
Revenue CentreChannel ✦
Chicken Shop Soho - Eat InEat In
Chicken Shop Shoreditch - Dine InEat In
Click & Collect CounterTakeaway
Deliveroo - London BridgeDelivery
UberEats IntegrationDelivery
Bar TabOther

Using regex_contains() in Card Filters

You can also use regex_contains in the Revenue Centre filter on a card — not just in formulas. This works the same way, but with one key difference:

💡 No quotes needed in the filter field When you paste a regex pattern directly into the Revenue Centre filter box, you do not put it in speech marks. Just type or paste the pattern directly: eat in|dine in|restaurant

In a formula, strings must be in double quotes: "eat in|dine in|restaurant"

⚡ Pro tip: Use AI to generate your regex patterns

If you have a large list of Revenue Centres, export them to a CSV and paste the list into an AI tool (like Claude). Ask it to: "Group these revenue centres into Eat In, Takeaway, Delivery and Other. Give me a regex pattern for each group using the pipe | operator."

This approach can reduce 65+ individual conditions down to just a handful of clean regex patterns — a big time-saver when you have multiple brands and locations.

Labour Cost Calculations

Labour % is one of the most tracked metrics in Tenzo. Here are the key patterns for calculating it accurately.

Example: Basic Labour %

Formula
round([Labour Cost] / [Net Sales] * 100, 1)

Example: Labour % vs a Target

Formula — flag sites over a 30% target
if(round([Labour Cost] / [Net Sales] * 100, 1) > 30,
  "⚠️ Over target", "✓ On target")

Example: Exclude agency workers from pension calculation

Formula — apply pension uplift only to employed staff
if(icontains([Pay Component], "agency"),
  [Labour Cost],
  [Labour Cost] * 1.03) // 3% pension uplift for employed staff only
Pay ComponentLabour CostPensionable Cost ✦
Kitchen£3,200£3,296
Front of House£2,800£2,884
Holiday Accrual£400£412
Agency Workers£1,500£1,500
Actual vs Forecast — using now()

Use now() combined with if() to display actual figures for past dates and forecast/planned figures for future dates. This is ideal for forward-looking dashboards.

Example: Blended Sales figure (actual + forecast)

Formula — use actual sales in the past, forecast going forward
if([Date] <= now(),
  [Actual Net Sales],
  [Forecast Sales])
DateActual SalesForecast SalesBlended Sales ✦
1 Apr (past)£8,200£8,000£8,200 ← actual
2 Apr (past)£7,600£7,800£7,600 ← actual
Today£6,900£7,200£6,900 ← actual
4 Apr (future)£8,100£8,100 ← forecast
5 Apr (future)£9,400£9,400 ← forecast

Example: Blended Labour Cost (actual + planned rota + target)

Formula — actual for past, planned rota for near future, target % beyond rota horizon
if([Date] <= now(), [Actual Labour Cost],
  if([Rota Scheduled] = "Yes", [Planned Labour Cost],
    [Forecast Sales] * 0.487)) // 48.7% labour target beyond rota horizon
💡 Adjusting the labour target % The 0.487 (48.7%) in the formula above is just a number — you can update it at any time to match your business's target. For example, use 0.30 for a 30% target.
Salary vs Hourly Workers

The Pay Rate Type dimension in Tenzo lets you differentiate between salaried and hourly employees. You can use this in calculated columns for more granular reporting.

Example: Hourly labour cost only

Formula — exclude salaried employees from a labour figure
if(icontains([Pay Rate Type], "hourly"), [Labour Cost], 0)

Example: Effective hourly rate

Formula — only meaningful for hourly workers
if(icontains([Pay Rate Type], "hourly"),
  round([Labour Cost] / [Hours Worked], 2),
  "N/A")
📌 Pay Rate Type dimension When building a card, look for Pay Rate Type in the Dimensions list. Adding it as a row grouping will automatically split your data into Salaried and Hourly rows — without needing a calculated column at all. Use a calculated column when you want to derive a new metric that treats the two groups differently.
Ranking — rank()

Add a rank column to quickly see which sites or categories are performing best or worst.

Example: Rank sites by Net Sales

Formula — 1 = highest
rank([Net Sales])
SiteNet SalesRank ✦
Soho£24,500#1
Shoreditch£22,000#2
Canary Wharf£19,800#3
💡 Combine rank with if() to highlight top performers
if(rank([Net Sales]) <= 3, "🏆 Top 3", "—")
Cumulative Sum — cum_sum()

Show a running total that builds up row by row. Useful for tracking sales progress toward a monthly target, or demand trends over time.

Example: Running total of weekly sales

Formula
cum_sum([Net Sales])
WeekNet SalesCumulative Sales ✦
Week 1£18,200£18,200
Week 2£21,400£39,600
Week 3£19,800£59,400
Week 4£22,100£81,500

Example: Track 12-week demand trend (inventory)

Formula — cumulative usage for a single SKU over 12 weeks
cum_sum([Inventory Usage])
💡 Useful for ordering cards Combine cum_sum() with max([Inventory Usage]) in a separate column to see both the trend and the single-day peak. Filter the card to one item and one location for meaningful results.
Advanced Example — Commission %

This example shows how to combine multiple functions to create a sophisticated commission calculation with tiered rates.

Scenario: Tiered commission on upsell revenue

Staff earn 5% commission on upsell revenue up to £500/week, and 8% above that. You want to show the earned commission per person.

Formula
if([Upsell Revenue] <= 500,
  round([Upsell Revenue] * 0.05, 2),
  round(500 * 0.05 + ([Upsell Revenue] - 500) * 0.08, 2))
Staff MemberUpsell RevenueCommission ✦
Alice£320£16.00 (5%)
Ben£500£25.00 (5%)
Chloe£750£45.00 (5%+8%)
Common Mistakes

These are the most frequent issues when building calculated columns — and how to fix them.

MistakeFix
% total showing wrong value
Total Row = Sum → adds all % values together
Set Total Row → Calculate for all percentage and ratio columns.
Text not in quotes
if([Labour %] > 30, High, Low)
if([Labour %] > 30, "High", "Low")
All text values must be in double quotes.
Column name typo
[net sales] — doesn't match "Net Sales"
Column names are case-sensitive. Copy them exactly from the card.
Grouped column not visible
Column disappears when rows are grouped
Turn on Apply to View in the column settings.
regex_contains quotes in filter
Pasting "eat in|delivery" into filter box
In the card filter field, paste without quotes: eat in|delivery. Quotes are only needed inside formulas.
Division giving no result
[Labour Cost] / [Net Sales] shows blank
If [Net Sales] is zero or null for some rows, division fails. Use if([Net Sales] > 0, [Labour Cost] / [Net Sales] * 100, 0).
Quick Reference
FunctionWhat it doesExample
sum(col)Sum of values in a columnsum([Labour Cost])
avg(col)Average of valuesavg([Daily Sales])
min(col)Minimum valuemin([Wage Rate])
max(col)Maximum valuemax([Inventory Usage])
rank(col)Rank rows by value (1 = highest)rank([Net Sales])
cum_sum(col)Running cumulative totalcum_sum([Net Sales])
round(x, n)Round to n decimal placesround([Labour %], 1)
round_up(x, n)Always round upround_up([Hours], 0)
round_down(x, n)Always round downround_down([Hours], 0)
if(cond, a, b)Return a if condition is true, else b. Nest up to 6 levels.if([Sales] > 0, "Active", "No Sales")
and(a, b)Both conditions must be trueand([Sales] > 0, [Labour] > 0)
or(a, b)Either condition must be trueor([Type] = "A", [Type] = "B")
contains(col, "x")Case-sensitive substring matchcontains([Site], "London")
icontains(col, "x")Case-insensitive substring matchicontains([Site], "london")
startswith(col, "x")Case-sensitive prefix matchstartswith([Code], "UK")
istartswith(col, "x")Case-insensitive prefix matchistartswith([Code], "uk")
regex_contains(col, pat)Regex match; use | for ORregex_contains([RC], "eat in|dine in")
now()Current date/timeif([Date] <= now(), [Actual], [Forecast])
// Highlight active sidebar link on scroll const sections = document.querySelectorAll('.section[id]'); const links = document.querySelectorAll('.sidebar a'); const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { links.forEach(l => l.classList.remove('active')); const active = document.querySelector('.sidebar a[href="#' + entry.target.id + '"]'); if (active) active.classList.add('active'); } }); }, { rootMargin: '-20% 0px -70% 0px' }); sections.forEach(s => observer.observe(s));