Quick reference for Tape calculation fields: syntax, functions, and recipes for the new editor.
- Part 1: Syntax & field references
- Part 2: Functions & HTML output
- Part 3: Dates, Markdown & recipes
- Part 4: Best practices & troubleshooting (this post)
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.
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:
- 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).
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
nullin arithmetic producesNaN. Default with?? 0.nullin string concat produces"null". Default with?? "".- A Date field returns a
Dateobject. To compare, usea > bora.getTime() > b.getTime(). - A Date Range field is an object with
.startand.endproperties. Both returnDateobjects, directly usable withmoment. Nonew Date(...)wrapper needed. - Avoid field names that match JavaScript globals. Names like
Math,Date,Number,String,Array,Object,JSONcause unpredictable failures. A field namedNumberis not injected as a variable at all. Arithmetic likeNumber * 1.19silently uses the JSNumberconstructor instead of your field value, returningNaN. 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/falsedirectly (e.g.Tags.includes("urgent")), you get “unhandled result type: boolean”. Convert it:Tags.includes("urgent") ? "Yes" : "No"orTags.includes("urgent").toString(). - A Relation field cannot be used as a JS array.
Tasksalone is undefined. Always go through.field.aggregation:Tasks.Amount.sum,Tasks.Title.all(Array of titles), then JS methods on the.allarray 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
.sumexclude empty values. Use.allWithNullinstead of.allif you need positions to be preserved. - Number output truncates by default. If your Number-type calculation shows
33instead of33.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.emailwhen there is no owner. UseOwner?.email). - A
Datefield 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")andparseFloat("3.9")work fine with string literals— 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 | |
| Close and reopen record | |
| Unreferenced field changes | |
| Referenced field changes | |
| Formula is saved |
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.
- Part 1: Syntax & field references
- Part 2: Functions & HTML output
- Part 3: Dates, Markdown & recipes
- Part 4: Best practices & troubleshooting (this post)