New calculation field cheat sheet - Part 4 best practices & troubleshooting

Quick reference for Tape calculation fields: syntax, functions, and recipes for the new editor.

We built a demo workspace to try things out. ➔ Duplicate it


Part 4 of 4. Best practices, hard limits, gotchas, and a full troubleshooting reference — including live-verified patterns that don’t work and their workarounds. For syntax basics see Part 1; for strings, arrays, and libraries see Part 2; for dates, Markdown, and recipes see Part 3.

:pushpin: 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, migration), see the announcement post in the Tape Community.

Two key rules in the new editor:

  1. No @ in saved code. @ is just a search trigger; the saved field reference is the bare name.
  2. Spaces become underscores. First name is written as First_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 RateHourly_Rate).


Best practices

Lessons baked in from common mistakes. Skim once, save yourself debugging time later.

Use the @ autocomplete. Don’t try to write the long @[Field name](field_ID) form by hand. Let the editor insert it. You also get rename safety for free.

Pick the output type once. Switching between Number / Text / Date later can break filters and views downstream. Decide before you build the rest of the app.

Prefer === over ==. Strict equality avoids silent type coercion bugs ("1" == 1 is true).

Default with ?? or ||. A field can be null for new records. Protect arithmetic and string ops:

Field types: Hours [Number], Hourly rate [Number]
Output type: Number

(Hours ?? 0) * Hourly_rate

Break long calculations into named variables.

Field types: Deadline [Date], Status [Single select]
Output type: Text

const days = moment(Deadline).diff(moment(), 'days')
const overdue = days < 0 && Status !== "Done"
overdue ? `⚠ Overdue by ${Math.abs(days)} days` : `${days} days left`

Use template literals for anything with more than two pieces. Easier to read, less prone to spacing bugs.

Keep the time budget in mind. Heavy .map → .filter → .reduce chains over hundreds of related records can time out. If you need that, consider a Number field updated by an automation instead.

Use _.get(...) for deep access. When you might be reading from a missing related record, lodash’s _.get(obj, "a.b.c", default) is shorter than nested ?..

Define helper functions at the top of long calculations. Tape doesn’t have global functions across calculations, but inside a single calculation you can declare reusable helpers. Cleans up repetition.

Field types: Start date [Date], Amount [Number]
Output type: Text

// Local helpers
const fmtMoney = n => n.toLocaleString("de-DE", { style: "currency", currency: "EUR" })
const fmtDate  = d => moment(d).format("DD.MM.YYYY")
const daysAgo  = d => moment(new Date()).diff(moment(d), 'days');
`${fmtDate(Start_date)} · ${fmtMoney(Amount)} · ${daysAgo(Start_date)} days ago`

Limits & gotchas

Hard ceilings and behavior quirks. Know these before you build something complex that hits a wall.

Limit Value Note
Field references per calculation 60 References across all field variables
Relation depth 10 levels How many hops through Relation fields
Async APIs Not supported No Promise, setTimeout, setInterval, fetch
Execution time Short Server-side budget. Long-running scripts time out and the field is marked invalid.
Output Last expression return is not allowed — last expression is the output

Common gotchas

  • null in arithmetic produces NaN. Default with ?? 0.
  • null in string concat produces "null". Default with ?? "".
  • A Date field returns a Date object. To compare, use a > b or a.getTime() > b.getTime().
  • A Date Range field is an object with .start and .end properties. Both return Date objects, directly usable with moment. No new Date(...) wrapper needed.
  • Avoid field names that match JavaScript globals. Names like Math, Date, Number, String, Array, Object, JSON cause unpredictable failures. A field named Number is not injected as a variable at all. Arithmetic like Number * 1.19 silently uses the JS Number constructor instead of your field value, returning NaN. Rename the field (e.g. Amount, Price, Revenue).
  • Boolean is not a valid output type. Output type can only be Text, Number, or Date. If your calculation returns true/false directly (e.g. Tags.includes("urgent")), you get “unhandled result type: boolean”. Convert it: Tags.includes("urgent") ? "Yes" : "No" or Tags.includes("urgent").toString().
  • A Relation field cannot be used as a JS array. Tasks alone is undefined. Always go through .field.aggregation: Tasks.Amount.sum, Tasks.Title.all (Array of titles), then JS methods on the .all array if needed (Tasks.Status.all.filter(s => s === "Done").length).
  • A Link field is an array of strings, even if it holds one URL. Use Website[0].
  • A Multi-select returns an array of label strings. Use .includes(...).
  • Single-select returns the label, not an internal ID. Renaming an option breaks calculations that compared against it.
  • Aggregations like .sum exclude empty values. Use .allWithNull instead of .all if you need positions to be preserved.
  • Number output truncates by default. If your Number-type calculation shows 33 instead of 33.33, the field’s Decimal Places setting is controlling the display. Set it to 2 (or more) in the field settings. The calculation itself is correct.
  • String-to-number conversion from Text field values is not supported. parseInt(), parseFloat(), Number(), and arithmetic coercion (+field, field * 1) all fail when applied directly to a Text field value. If you need a number, use a Number field instead of a Text field.
  • Calculation fields are server-computed: you cannot write to them via the API.
  • Recompute order is not guaranteed across multiple calculation fields. Don’t chain calculations that depend on each other to update in a specific order in the same write. Split into one calculation that references the inputs directly.

Troubleshooting

Symptom → cause → fix. Covers the errors you will actually hit in practice.

Field shows “invalid” / red error. The script threw or timed out. Most common causes:

  • Reading a property of null (e.g. Owner.email when there is no owner. Use Owner?.email).
  • A Date field referenced as a string (or vice versa).
  • More than 60 references in one script.
  • A loop over a very large relation array.

Result is NaN. A number is null somewhere in the math. Add ?? 0 to defaults.

Result is "undefined" or "null". A string concatenation hit a missing field. Switch to template literals + ??:

Field types: Full name [Text], Nickname [Text]
Output type: Text

(Full_name ?? "—") + " (" + (Nickname ?? "—") + ")"

HTML / Markdown not rendering. Check the output type is Text. Number/Date types render the value verbatim.

Date shows as “Invalid Date”. You probably parsed a string the wrong way:

new Date("2026-04-30")          // ✅
new Date("30.04.2026")          // ❌ ambiguous
moment("30.04.2026", "DD.MM.YYYY").toDate()  // ✅

Script works for some records, fails for others. Almost always a null field on the failing records. Add defaults.

Performance is slow. Reduce the number of references and avoid nested map/filter over large arrays. If you really need to crunch many records, move the work to an automation that writes a Number field.


What doesn’t work

Live-verified patterns that fail in the current editor, with confirmed workarounds. All entries were tested in the new record experience.

return is not allowed

return "hello"
// ❌ Syntax error: Illegal return statement

Calculations run as an expression body. The last evaluated expression is the output — no return needed or allowed.

"hello"         // ✅ bare value

const x = "hello"
x               // ✅ variable, last expression

Output types: boolean and array not accepted

Boolean:

Status === "Done"
// ❌ The script returned an unhandled result type: "boolean"

Tags.includes("urgent")
// ❌ The script returned an unhandled result type: "boolean"

Workaround: wrap in a ternary:

Status === "Done" ? "Yes" : "No"
Tags.includes("urgent") ? "Yes" : "No"
Hours >= 40 ? "Overtime" : "Regular"

Array (.all / .allWithNull):

Tasks.Title.all
// ❌ The script returned an unhandled result type: "object"

Workaround: chain an array method:

Tasks.Title.all.join(", ")                              // ✅
Tasks.Title.all.length                                  // ✅
Tasks.Status.all.filter(s => s === "Done").length       // ✅

Date Range fields: always use .start or .end

Sprint
// ❌ Script reference error: Sprint is not defined

Date Range fields have no standalone value. Always access .start or .end:

Sprint.start          // ✅ Start date as Date object
Sprint.end            // ✅ End date as Date object
Sprint.start != null  // ✅ Check if set

The object shorthand pattern { Field1, Field2 }[name] does not work with Date Range fields. Only Text, Number, Date (single), and Single Select fields are compatible.

ASI trap: template literal without semicolon

JavaScript’s Automatic Semicolon Insertion does not fire when a new line starts with a backtick. The previous expression is interpreted as a function call with the template literal as its argument.

const x = 42
`Result: ${x}`
// ❌ Cannot access 'x' before initialization
var x = 42
`Result: ${x}`
// ❌ 42 is not a function

Fix: always add a semicolon before a template literal used as the return value:

const x = 42;
`Result: ${x}`   // ✅

name — reserved variable in Tape

name is already defined in Tape’s JavaScript environment. Combined with the ASI trap above, const name = "..." causes “Cannot access ‘name’ before initialization”.

Workaround: use specific variable names — appName, labelText, outputText instead of name.

Wrong bracket position with moment().diff()

moment(Deadline).diff(moment(new Date(), 'days'))
// ❌ Wrong — 'days' is inside moment(), not inside .diff()
moment(Deadline).diff(moment(), 'days')
// ✅ moment() = now, 'days' as second argument to .diff()

String-to-number conversion from Text fields

parseInt(Note)      // ❌ The result is not a valid number.
parseFloat(Note)    // ❌ The result is not a valid number.
+Note               // ❌ The result is not a valid number.
Note * 1            // ❌ The result is not a valid number.

Tape returns field values as proxy objects. typeof reports "string", but numeric conversion functions do not accept these proxy objects.

Workaround: use a Number field instead of a Text field.

parseInt("42") and parseFloat("3.9") work fine with string literals :white_check_mark: — only field references fail.

Recompute behavior

Calculation fields recompute only when a referenced field changes or when the formula is saved.

Trigger Recomputes?
Page reload :x: No
Close and reopen record :x: No
Unreferenced field changes :x: No
Referenced field changes :white_check_mark: Yes
Formula is saved :white_check_mark: Yes

A calculation referencing no fields (e.g. just new Date()) will never auto-update — it always shows the value from the last save. To capture the moment a field changes, use an Automation, not a Calculation field.

Field names that shadow JS built-ins

// Field named "Number" (Number type, value: 10011)
Number * 1.19
// ❌ The result is not a valid number.
// Tape does not inject the field — Number remains the JS Number() constructor

Fix: rename the field. Use Amount, Revenue, Price, Count instead.

Same risk applies to: String, Date, Math, Array, Object, JSON.

date_fns — lowercase tokens required

date_fns is available but requires lowercase format tokens (v2 standard). Year must be yyyy, not YYYY.

Wrong (moment-style) Correct (date_fns v2)
'YYYY-MM-DD' 'yyyy-MM-dd'
'DD.MM.YYYY' 'dd.MM.yyyy'
'MMMM D, YYYY' 'MMMM d, yyyy'
date_fns.format(new Date(), 'yyyy-MM-dd')   // ✅
date_fns.format(new Date(), 'YYYY-MM-DD')   // ❌ Wrong year token

Access via date_fns.*. Global calls without the prefix fail.

Email field: not a plain string

`${First_name} ${Last_name} <${Email}>`
// ❌ Renders as: "Juliet Adams <[object Object]>"

Email returns an object, not a plain string. Access pattern not fully verified — avoid using Email directly in string expressions.

Contact field: not an object, no .email, no .name

Owner.email   // ❌ undefined
Owner.name    // ❌ undefined

A Contact field (Member type) returns a string array of display names:

Owner[0]            // ✅ "Juliet Adams"
Owner.length        // ✅ number of assigned members
Owner.join(", ")    // ✅ "Juliet Adams, Matt Brewer"
Owner?.[0] ?? "—"  // ✅ safe access with fallback

Email, avatar, user ID, and role are not accessible — only the display name.

Built-ins not yet available

Created_on
Last_edited_on
// ❌ Not yet available — coming soon

Resources


Found something that doesn’t work and isn’t listed here? Reply below.


2 Likes