Quick reference for Tape calculation fields: syntax, functions, and recipes for the new editor.
- Part 1: Syntax & field references (this post)
- Part 2: Functions & HTML output
- Part 3: Dates, Markdown & recipes
- Part 4: Best practices & troubleshooting
We built a demo workspace to try things out. β Duplicate it
A calculation field runs JavaScript on every record. This is Part 1 of 4: syntax, field references, operators, empty/null handling, and control flow.
This cheat sheet covers the new calculation editor in the new record experience. For details on the editor itself (autocomplete, live preview, fullscreen, Find & Replace, multi-cursor, classic vs new editor, migration), see the announcement post in the Tape Community.
Two key rules in the new editor:
- No
@in saved code.@is just a search trigger; the saved field reference is the bare name.- Spaces become underscores.
First nameis written asFirst_name.
Each section and recipe shows the fields it uses with their type. Replace the field names with your own; spaces in your field name become underscores in the code (e.g.
Hourly RateβHourly_Rate).
Terminology
| Term | Meaning |
|---|---|
| Field | Stored value on a record (e.g. Status, Amount) |
| Property | Tape built-in (e.g. Record_ID) |
| Variable | Field reference inside calculation code |
| Aggregation | Computed value across related records (.sum, .all, .avg, .min, .max, .allWithNull) |
| Relation | Field type linking to another app. Accessed via Relation.Field.aggregation. |
| Output type | Text / Number / Date. Set on the calculation field; determines how the result is displayed and used. |
Quick start
Add a Calculation field, type @ to pick a field from the autocomplete (or just start typing the field name; suggestions appear on their own in the new editor), and write any JavaScript expression. The last expression in the script is what gets stored.
Example: in an Orders app, a Total cost calculation that multiplies Price Γ Quantity:
Field types: Price [Number], Quantity [Number]
Output type: Number
// Total cost
Price * Quantity
Field types: First name [Text], Last name [Text]
Output type: Text
// Concatenate two text fields
First_name + " " + Last_name
Field types: Deadline [Date]
Output type: Number
// Days until a deadline
moment(Deadline).diff(moment(), 'days')
Field types: Score [Number]
Output type: Text
// Conditional badge
Score >= 80 ? "Pass" : "Fail"
That is the whole mental model. Everything below is detail.
How it works
The technical model behind every calculation. Read once, then forget. The rules are simple and consistent.
| Aspect | Detail |
|---|---|
| Language | JavaScript, ES2021 (Node.js v18 on the server) |
| Execution | Server-side, on every create/update of the record |
| Live in record creation | In the new editor: calculations run while you fill in fields on a new record |
| Live in forms | In the new editor: calculations update live as users fill out a Tape form |
| Async | Not supported. No Promise, no setTimeout, no setInterval, no fetch |
| Time budget | Synchronous and short. Long-running scripts time out and the field is marked invalid |
| Output | Plain text, number, Markdown, HTML/CSS. Set per field. |
| Triggers automations | Yes. Per-field toggle: turn off while building, back on when ready. |
| Triggers webhooks | Yes. Per-field toggle; same idea, controls outbound traffic. |
| Storage | The result is stored on the record, not recomputed on read |
| Save without a variable | Allowed. Useful for static templates, constants, dividers. |
The script behaves like an expression body: write expressions, declare variables with const/let, and the last evaluated expression becomes the field value. return is not allowed.
// Both patterns work
"hello"
// or
const greeting = "hello"
greeting
Editor features β autocomplete, live preview, fullscreen, Find & Replace, multi-cursor, and saving β are covered in the announcement post β.
Field references
How to read values from other fields.
In the new editor: type the field name directly; autocomplete picks it up. Spaces in the field name become underscores. Press @ to open a searchable list (variables = your fields, properties = Tape built-ins). The @ is not part of the final code.
Hover any field token in the editor to see its type, app, value description, and Workspace/App/Field IDs.
In the classic editor: every reference starts with @ followed by the field name. Type @ to autocomplete.
Naming conversion
Field names are taken 1:1 from your app. Case is preserved. Only spaces are replaced with underscores.
| Field is named β¦ | In the new editor β¦ | In the classic editor β¦ |
|---|---|---|
First name |
First_name |
@First name |
Hourly Rate |
Hourly_Rate |
@Hourly Rate |
Contracts |
Contracts |
@Contracts |
Relation to Old |
Relation_to_Old |
@Relation to Old |
Same-app references
The simplest case: pull a value from a field in the same record. Examples below use the new-editor form.
Field types: Title [Text], Quantity [Number], Status [Single select], Record_ID [built-in Number]
Title
Quantity
Status
Record_ID
Behind the scenes Tape stores these as @[Field name](field_ID) so you can rename a field without breaking the calculation. You will see the long form in the API; in the editor the short form is what you type.
Field types and what they return
Each field type comes back as a specific JavaScript value. Knowing this avoids βis that a string or an array?β surprises.
| Field type | Returns | How to use it |
|---|---|---|
| Text | string | Title β "Project Phoenix" |
| Long text | string (Markdown) | Description β "# Goals\n..." |
| Number | number | Hours β 42.5 |
| Money | number | Amount β 199.99 |
| Date (single) | Date object | Deadline. Directly usable with moment. |
| Date (range) | Object with .start and .end |
Sprint.start, Sprint.end β both Date objects |
| Yes/No | boolean | Active β true (not a valid output type. Convert to text/number) |
| Single select | string (label) | Status β "Completed" |
| Multi select | string[] (labels) |
Tags β ["urgent", "client"]. Use .includes(), .length directly. |
| object |
Returns an object, not a plain string β access pattern unverified. Avoid in string expressions. | |
| Phone | string | Phone β "+49..." |
| Link / URL | string[] |
Website β ["https://..."] |
| Contact (member) | string[] (display names) |
Owner[0] β "Juliet Adams", Owner.length, Owner.join(", ") |
| Relation | Not a JS array. Only accessible via .field.aggregation |
Tasks.Title.all, Invoices.Amount.sum (see below) |
Built-in Record_ID |
number | Record_ID β 173763603 |
Built-in properties
Tape exposes a small set of built-in properties next to your fields. Press @ in the editor and look at the Properties section of the autocomplete (separate from the Variables section, which lists your fields).
| Property | Returns | Status |
|---|---|---|
Record_ID |
The unique numeric ID of the current record | Available |
Created_on |
When the record was created (Date) | Coming soon |
Last_edited_on |
When the record was last edited (Date) | Coming soon |
Created_by |
Who created the record (Member) | Coming soon |
Last_edited_by |
Who last edited the record (Member) | Coming soon |
Field types: Record_ID [built-in Number]
Output type: Text
// Use the record ID in a formatted code
"PRJ-" + String(Record_ID).padStart(5, "0")
// β "PRJ-00042"
Aggregations across related records
Pull a computed value across all linked records: sum of invoices, list of titles, average rating.
In the new editor: type the relation field, then ., then a field, then . again to step into aggregations like .all and .allWithNull. Build chains directly: Invoices.Amount.sum, Tasks.Title.all. The exact list of available aggregations is shown by the editorβs autocomplete after the second ..
In the classic editor: pick @All of β¦, @Sum of β¦ etc. from the autocomplete after @.
| Classic editor | New editor | Meaning |
|---|---|---|
@All of Title |
Tasks.Title.all |
Array of titles from all related records |
@All of Title with nulls |
Tasks.Title.allWithNull |
Same, but keeps positions for empty values |
@Sum of Amount |
Invoices.Amount.sum |
Sum across all related records |
@Average of Amount |
Invoices.Amount.avg |
Average |
@Minimum of Amount |
Invoices.Amount.min |
Minimum |
@Maximum of Amount |
Invoices.Amount.max |
Maximum |
Aggregations work for both outgoing and incoming relations.
Field types: Invoices [Relation β Amount (Number)]
Output type: Number
// Total of all linked invoices
Invoices.Amount.sum
Field types: Reviews [Relation β Rating (Number)]
Output type: Number
// Average rating across all linked reviews
Reviews.Rating.avg
Field types: Tasks [Relation β Title (Text)]
Output type: Text
// Show all related task titles, comma-separated
Tasks.Title.all.join(", ")
Limits: A single Calculation field can hold up to 60 field references and reach 10 levels of relation depth.
Output types
Decides how the result is stored, formatted, and used elsewhere in Tape (filters, calendar, sums). Set it once when you create the field. Change it later only if needed, since some downstream views rely on it.
| Output type | When to use |
|---|---|
| Text | Concatenation, formatted output, Markdown, HTML |
| Number | Arithmetic, sums, percentages, money |
| Date | Computed dates and times |
Date formatting
When the output type is Date, return a Date object. The display picks up the userβs locale.
Field types: Start date [Date]
Output type: Date
moment(Start_date).add(14, 'days').toDate()
If you want a custom string format, switch the output type to Text and format the date yourself:
Field types: Signed date [Date]
Output type: Text
moment(Signed_date).format("MMMM D, YYYY")
// β "April 30, 2026"
Operators
The building blocks for almost every calculation: math, comparisons, and combining conditions.
Arithmetic
Standard math on numbers. + also concatenates text.
| Operator | Meaning | Example |
|---|---|---|
+ |
Add (or concat strings) | Revenue + Bonus |
- |
Subtract | Revenue - Cost |
* |
Multiply | Rate * Hours |
/ |
Divide | Gross / 1.19 |
% |
Remainder | Record_ID % 2 |
** |
Power | Radius ** 2 |
Field types: Gross [Number], Revenue [Number], Cost [Number]
Output type: Number
// Net price from gross
Gross / 1.19
// Margin percentage
((Revenue - Cost) / Revenue) * 100
Comparison
Returns true or false. Use these inside a ternary, if, or array filter β not as standalone output (Tape doesnβt accept boolean as an output type).
| Operator | Meaning |
|---|---|
=== |
Equal (strict, preferred) |
!== |
Not equal (strict) |
==, != |
Equal / not equal (loose, avoid) |
>, >=, <, <= |
Numeric comparison |
Field types: Status [Single select], Hours [Number]
Output type: Text
Status === "Done" ? "Yes" : "No"
Hours >= 40 ? "Overtime" : "Regular"
Logical
Combine multiple conditions or fall back when a value is missing.
| Operator | Meaning | Example |
|---|---|---|
&& |
And | Revenue > 0 && Cost > 0 |
|| |
Or | Status === "Active" || Status === "Pending" |
! |
Not | !Archived |
?? |
Nullish coalescing | Nickname ?? Full_name |
?. |
Optional chaining | Owner?.email |
Field types: Nickname [Text], Full name [Text]
Output type: Text
// Use Nickname if present, otherwise Full name
Nickname ?? Full_name
Field types: Primary contact [Contact]
Output type: Text
// Name of the first assigned contact, "β" if empty
Primary_contact?.[0] ?? "no contact"
String concatenation
Glue text and field values together with +, or use template literals (backticks) for complex multi-line strings.
Field types: First name [Text], Last name [Text]
Output type: Text
First_name + " " + Last_name
Field types: First name [Text], Last name [Text], Title [Text]
Output type: Text
Last_name + ", " + First_name + " β " + Title
Empty & null values
A field that has no value comes through as null (or sometimes undefined). Plain JavaScript is forgiving in some places and strict in others. These are the patterns to know.
Detect βemptyβ
Check whether a field is unset or blank before reading it. These return true/false β use them inside a ternary or if, not as standalone output.
| Check | Matches |
|---|---|
x == null |
null and undefined (the safe default) |
!x |
null, undefined, "", 0, false, NaN (broad; be careful with numbers) |
x === "" |
Empty string only |
x.length === 0 |
Empty string or empty array |
Array.isArray(x) && x.length === 0 |
Empty array (safe) |
Field types: Title [Text]
Output type: Text
// Field is set?
Title != null && Title !== "" ? Title : "Untitled"
Field types: Tags [Multi select]
Output type: Text
// Array has items?
Array.isArray(Tags) && Tags.length > 0 ? Tags.join(", ") : "β"
Provide defaults
Substitute a fallback when a field is empty, so your math and templates donβt break.
| Pattern | When to use |
|---|---|
Hours ?? 0 |
Use 0 if null/undefined. Preferred. |
Hours || 0 |
Use 0 if any falsy value (incl. 0, ""). Use only when you really mean it. |
Title ?? "Untitled" |
String fallback |
Tags ?? [] |
Empty array fallback |
Field types: Hours [Number], Rate [Number]
Output type: Number
// Safe arithmetic
(Hours ?? 0) * (Rate ?? 0)
Field types: Name [Text], Nickname [Text]
Output type: Text
// Safe concatenation
(Name ?? "β") + " (" + (Nickname ?? "no nickname") + ")"
Return βemptyβ
Make the field render as blank in the UI. Use this when a value isnβt applicable yet.
| Output type | How to return empty |
|---|---|
| Text | "" (empty string) |
| Number | Either return 0, or use Text output and return "" to hide it |
| Date | Return a Date only when applicable; for βno dateβ use Text output instead |
Field types: Title [Text]
Output type: Text
// Show a warning string, or nothing
Title ? "" : "β Title missing"
Field types: Status [Single select], Opened on [Date]
Output type: Text
// Conditional value, blank when not applicable
Status === "Done" ? "" : `Open since ${moment(Opened_on).format("MMM D")}`
Optional chaining for nested values
Safely read a property on a related record. The ?. short-circuits to undefined if the parent is missing. No crash.
Field types: Primary contact [Contact]
Output type: Text
// Name of the first assigned contact, "β" if none
Primary_contact?.[0] ?? "β"
Field types: Owner [Contact]
Output type: Text
// First assigned member name, with fallback
_.get(Owner, "[0]", "β")
Control flow
Pick which value to return based on conditions. Five patterns, from shortest to most flexible.
Ternary (single-line if/else)
The fastest way to choose between two values. Format: condition ? whenTrue : whenFalse.
Field types: Score [Number]
Output type: Text
Score >= 80 ? "Pass" : "Fail"
Chained ternary (avoid more than 3 levels)
Multiple cases in a single expression. Reads top-to-bottom like a sequence of βif X, then β¦β.
Field types: Score [Number]
Output type: Text
Score >= 90 ? "A" :
Score >= 80 ? "B" :
Score >= 70 ? "C" : "D"
if / else if / else
Use when each branch needs multiple steps, intermediate variables, or longer logic.
Field types: Hours [Number]
Output type: Text
let label
if (Hours > 40) {
label = "Overtime"
} else if (Hours > 0) {
label = "Regular"
} else {
label = "Not logged"
}
label
switch
Pick a value based on an exact match. Faster to scan than long if/else chains.
Field types: Status [Single select]
Output type: Text
let label
switch (Status) {
case "new": label = "π New"; break
case "active": label = "π In progress"; break
case "done": label = "β
Done"; break
default: label = "β Unknown"
}
label
Lookup table (cleaner than switch for many cases)
Map keys directly to values in an object. Cleanest pattern when you have many simple cases.
Field types: Status [Single select]
Output type: Text
const labels = {
new: "π New",
active: "π In progress",
done: "β
Done",
blocked: "β Blocked"
}
labels[Status] ?? "β Unknown"
Resources
Spotted a missing field type, operator, or syntax rule? Reply below.
- Part 1: Syntax & field references (this post)
- Part 2: Functions & HTML output
- Part 3: Dates, Markdown & recipes
- Part 4: Best practices & troubleshooting