Card Creator — Calculated Columns Guide
Calculated Columns in Card Creator
Calculated columns let you build custom metrics directly inside your Tenzo cards — without any spreadsheet exports. This guide walks through real-world examples so you can get started quickly.
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.
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
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.
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
Filter conditions
| Condition | What it does | Example use case |
|---|---|---|
| In | Exact match — shows only the specified value(s) | Show only "Shoreditch" location |
| Not In | Exact match — excludes the specified value(s) | Exclude "Test Location" from all data |
| Contains | Partial match — includes any value with this text | Show all locations with "Kiosk" in the name |
| Does Not Contain | Partial match — excludes any value with this text | Exclude all kiosks from the report |
| regex_contains | Regex pattern — use | for OR matching | Match "eat in|dine in|restaurant" in one go |
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:
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:
| Scenario | What 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. |
Practical examples
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.
These settings control how your calculated column behaves and displays. Getting them right makes a big difference.
Data Format
Controls the visual style of values. Use Percent Color for %, Currency Color for £/$, Left Justified for text labels.
Total Row
Set to Sum for additive values. Set to Calculate for ratios and percentages — otherwise the total will be wrong.
Row Type
Controls how subtotals are computed: SUM adds values, MIN shows the minimum. Use MIN for rate columns like wage rates.
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.
The simplest calculated columns just add, subtract, multiply or divide existing columns. Column names are always wrapped in square brackets.
Example: Gross Profit
| Date | Net Sales | Cost of Goods | Gross 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
round([Net Sales] / [Covers], 2) to limit to 2 decimal places.
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
| Site | Labour Cost | Net Sales | Labour % ✦ |
|---|---|---|---|
| Shoreditch | £6,200 | £22,000 | 28.2% |
| Soho | £7,800 | £24,500 | 31.8% |
| Canary Wharf | £5,400 | £19,800 | 27.3% |
| Total | £19,400 | £66,300 | 29.3% |
Example: Sales Mix %
if()Use if() to return different values depending on a condition. You can nest up to 6 if() statements for more complex logic.
Syntax
Example: Flag high labour %
| Site | Labour % | Status ✦ |
|---|---|---|
| Shoreditch | 28.2% | ✓ OK |
| Soho | 31.8% | ✓ OK |
| Canary Wharf | 38.5% | ⚠️ High |
Example: Tiered performance band (nested if)
if([Labour %] < 30, "🟡 Good",
if([Labour %] < 35, "🟠 Watch", "🔴 Over")))
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
if(icontains([Revenue Centre], "takeaway"), "Takeaway",
if(icontains([Revenue Centre], "eat in"), "Eat In", "Other")))
| Original Revenue Centre | Grouped Category ✦ |
|---|---|
| Deliveroo - Chicken Shop (London) | Delivery |
| UberEats - Takeaway Counter | Takeaway |
| Restaurant Eat In - Main | Eat In |
| Bar Revenue | Other |
Available text matching functions
| Function | Behaviour | Example |
|---|---|---|
contains(col, "text") | Case-sensitive match anywhere in string | Matches "Delivery" not "delivery" |
icontains(col, "text") | Case-insensitive match anywhere | Matches "Delivery" and "delivery" |
startswith(col, "text") | Case-sensitive, matches start | Matches "Uber..." not "My Uber..." |
istartswith(col, "text") | Case-insensitive, matches start | Matches "uber..." and "Uber..." |
regex_contains(col, pattern) | Full regex, pipe | for OR | See Regex section below |
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.
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
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 Centre | Channel ✦ |
|---|---|
| Chicken Shop Soho - Eat In | Eat In |
| Chicken Shop Shoreditch - Dine In | Eat In |
| Click & Collect Counter | Takeaway |
| Deliveroo - London Bridge | Delivery |
| UberEats Integration | Delivery |
| Bar Tab | Other |
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:
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 % is one of the most tracked metrics in Tenzo. Here are the key patterns for calculating it accurately.
Example: Basic Labour %
Example: Labour % vs a Target
"⚠️ Over target", "✓ On target")
Example: Exclude agency workers from pension calculation
[Labour Cost],
[Labour Cost] * 1.03) // 3% pension uplift for employed staff only
| Pay Component | Labour Cost | Pensionable Cost ✦ |
|---|---|---|
| Kitchen | £3,200 | £3,296 |
| Front of House | £2,800 | £2,884 |
| Holiday Accrual | £400 | £412 |
| Agency Workers | £1,500 | £1,500 |
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)
[Actual Net Sales],
[Forecast Sales])
| Date | Actual Sales | Forecast Sales | Blended 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)
if([Rota Scheduled] = "Yes", [Planned Labour Cost],
[Forecast Sales] * 0.487)) // 48.7% labour target beyond rota horizon
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.
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
Example: Effective hourly rate
round([Labour Cost] / [Hours Worked], 2),
"N/A")
rank()Add a rank column to quickly see which sites or categories are performing best or worst.
Example: Rank sites by Net Sales
| Site | Net Sales | Rank ✦ |
|---|---|---|
| Soho | £24,500 | #1 |
| Shoreditch | £22,000 | #2 |
| Canary Wharf | £19,800 | #3 |
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
| Week | Net Sales | Cumulative 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)
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.
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.
round([Upsell Revenue] * 0.05, 2),
round(500 * 0.05 + ([Upsell Revenue] - 500) * 0.08, 2))
| Staff Member | Upsell Revenue | Commission ✦ |
|---|---|---|
| Alice | £320 | £16.00 (5%) |
| Ben | £500 | £25.00 (5%) |
| Chloe | £750 | £45.00 (5%+8%) |
These are the most frequent issues when building calculated columns — and how to fix them.
| Mistake | Fix |
|---|---|
|
% 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). |
| Function | What it does | Example |
|---|---|---|
sum(col) | Sum of values in a column | sum([Labour Cost]) |
avg(col) | Average of values | avg([Daily Sales]) |
min(col) | Minimum value | min([Wage Rate]) |
max(col) | Maximum value | max([Inventory Usage]) |
rank(col) | Rank rows by value (1 = highest) | rank([Net Sales]) |
cum_sum(col) | Running cumulative total | cum_sum([Net Sales]) |
round(x, n) | Round to n decimal places | round([Labour %], 1) |
round_up(x, n) | Always round up | round_up([Hours], 0) |
round_down(x, n) | Always round down | round_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 true | and([Sales] > 0, [Labour] > 0) |
or(a, b) | Either condition must be true | or([Type] = "A", [Type] = "B") |
contains(col, "x") | Case-sensitive substring match | contains([Site], "London") |
icontains(col, "x") | Case-insensitive substring match | icontains([Site], "london") |
startswith(col, "x") | Case-sensitive prefix match | startswith([Code], "UK") |
istartswith(col, "x") | Case-insensitive prefix match | istartswith([Code], "uk") |
regex_contains(col, pat) | Regex match; use | for OR | regex_contains([RC], "eat in|dine in") |
now() | Current date/time | if([Date] <= now(), [Actual], [Forecast]) |