> ## Documentation Index
> Fetch the complete documentation index at: https://learn.nextedy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add a Computed Column

> Derive a column's content from an expression in Nextedy POWERSHEET — either stored back to an entity property, or displayed only, with no property behind it.

export const LastReviewed = ({date}) => {
  if (!date) return null;
  const formatted = new Date(`${date}T00:00:00Z`).toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
    timeZone: "UTC"
  });
  return <p className="mt-10 text-sm text-gray-400 dark:text-zinc-500 not-prose">
      Last reviewed on {formatted}
    </p>;
};

A **computed column** derives its content from a `value` expression instead of reading a stored
property directly. Powersheet computes it in the browser every time the sheet renders.

There are two kinds, and one character in the binding decides which you get:

* a **stored** computed column writes its result back to the property it binds to, and
* a **display-only** computed column stores nothing — it exists in the sheet and nowhere else.

## Prerequisites

* A working sheet configuration YAML file
* A data model whose properties you want to compute from
* Familiarity with [dynamic value expressions](/powersheet/reference/sheet-config/dynamic-expressions) (`() =>` syntax)

## Decide Whether the Value Should Be Stored

The binding key decides. A key prefixed with `$` is **unbound**: it names a column, not a property.

| Binding  | `value` | Result                                                                                                                          |
| -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `total`  | not set | Ordinary stored column — the user edits it                                                                                      |
| `total`  | set     | Computed **and stored** — the result is written to `total`, marks the row modified, and persists on save. Read-only to the user |
| `$total` | set     | Computed and **displayed only** — nothing is stored, nothing is saved. Read-only to the user                                    |
| `$total` | not set | Configuration error — an unbound column has nothing to derive its content from                                                  |

<Warning>
  **`value` is about data; `render` and `display` are about appearance.**

  Do not reach for `render` to avoid writing data — `render` styles whatever the cell already holds.
  If the value should never be stored, make the column unbound with `$`. If it should be stored,
  bind it to a real property.
</Warning>

<Steps>
  <Step title="Add a Stored Computed Column">
    Bind the column to a real property and give it a `value` expression. The result is written to that
    property, so it saves with the rest of the row:

    ```yaml theme={null}
    columns:
      total:
        title: Total
        value: "() => context.entity.count * context.entity.rate"
        format: "c0$"
    ```

    The user cannot type into the column — a computed column is read-only, because its content comes
    from the expression rather than from input. Editing `count` or `rate` recomputes it, marks
    the row modified, and the new total persists on the next save.
  </Step>

  <Step title="Add a Display-Only Computed Column">
    Prefix the binding key with `$` to name a column that has no property behind it:

    ```yaml theme={null}
    columns:
      $riskScore:
        title: Risk Score
        value: "() => context.entity.Probability * context.entity.Severity"
        valueType: number
    ```

    Nothing is written anywhere. The column appears in the sheet, is read-only, and leaves the work
    item untouched — saving the row does not create or update a `riskScore` field.

    The `$` marker belongs on the **last** segment only. Everything before it is an ordinary path that
    must resolve in the data model, and it decides which entity the column reads from:

    ```yaml theme={null}
    columns:
      # derives from the root entity
      $riskScore:
        value: "() => context.entity.Probability * context.entity.Severity"

      # derives from the expanded system requirement
      systemRequirements.systemRequirement.$coverage:
        value: "() => context.entity.verifiedCount + ' / ' + context.entity.totalCount"
    ```

    <Tip>
      **`$total` and `total` can coexist.**

      The marker stays part of the column's internal identity, so a display-only `$total` does not
      collide with a real `total` property in the same sheet. Give them different `title` values so
      readers can tell them apart.
    </Tip>

    If you leave `title` out, the header shows the name without the marker — `$riskScore` displays as
    `riskScore`.
  </Step>

  <Step title="Choose the Result Type">
    A display-only column has no metadata to take its type from, so declare it with `valueType`:

    | `valueType` | Use for                      | Notes                                                    |
    | ----------- | ---------------------------- | -------------------------------------------------------- |
    | `string`    | Text, labels, concatenations | The default when `valueType` is omitted                  |
    | `number`    | Scores, counts, sums, prices | Enables `format` and `aggregate`                         |
    | `date`      | Dates and date-times         | Formats as `MMM dd, yyyy` unless `format` says otherwise |

    ```yaml theme={null}
    columns:
      $riskScore:
        title: Risk Score
        value: "() => context.entity.Probability * context.entity.Severity"
        valueType: number
        format: "n0"

      $reviewDue:
        title: Review Due
        value: "() => context.entity.approvedOn"
        valueType: date
        format: "yyyy-MM-dd"
    ```

    <Note>
      **`valueType` is for unbound columns only.**

      A bound column takes its type from the data model, so declaring `valueType` on one is rejected.
      And because there is no metadata to consult, `valueType: date` cannot distinguish a date from a
      date-time — set `format` if you need the time part shown or hidden.
    </Note>
  </Step>

  <Step title="Total a Numeric Column in Group Rows">
    A numeric display-only column can be aggregated into group rows like any other numeric column:

    ```yaml theme={null}
    columns:
      $riskScore:
        title: Risk Score
        value: "() => context.entity.Probability * context.entity.Severity"
        valueType: number
        aggregate: sum
        groupBy: true
    ```

    `aggregate` requires `valueType: number` — declaring it on a text or date column is rejected.
  </Step>

  <Step title="Read the Row and the Level Above">
    Inside an unbound column's `value` expression you can reach past the current entity:

    | Expression       | Gives you                                                                   |
    | ---------------- | --------------------------------------------------------------------------- |
    | `context.entity` | The entity at the column's own level — the one the binding path resolves to |
    | `context.source` | The entity one level up, the one this level was navigated from              |
    | `context.row`    | The whole row, as `context.row.entities[<level>]` across every level        |

    ```yaml theme={null}
    columns:
      systemRequirements.systemRequirement.$origin:
        title: Origin
        value: "() => `${context.source.id} → ${context.entity.id}`"
    ```

    <Warning>
      **`context.value` is empty in a `value` expression.**

      A computed column has no stored cell value to start from, so `context.value` is `undefined`.
      Read the properties you need from `context.entity` instead.
    </Warning>
  </Step>
</Steps>

## What a Display-Only Column Cannot Do

A display-only column stores nothing and resolves no metadata, so several column properties do not
apply to it. Each one is rejected outright rather than ignored:

| Property                                | Why it is rejected                                                   |
| --------------------------------------- | -------------------------------------------------------------------- |
| `isRequired`                            | The column is read-only and holds no stored data                     |
| `multiItem`                             | It binds no navigation property                                      |
| `list`                                  | The stages that resolve enumerations and references never run for it |
| `valueType` on a **bound** column       | A bound column takes its type from the data model                    |
| `aggregate` without `valueType: number` | Only numeric derived values aggregate meaningfully                   |

`render`, `renderers`, `format`, `groupBy`, `sort`, `filter`, `columnGroup`, and the styling
properties all work normally.

## Verify

After saving the configuration, do a full browser reload or re-open the document — the in-sheet
refresh button reloads data, not the configuration. You should now see:

* The computed column shows its derived value on every row
* Clicking a cell does not enter edit mode, and pasting into the column changes nothing
* Editing a property the expression reads updates the derived cell immediately
* For a **stored** computed column, the row is marked modified and the value persists after save
* For a **display-only** column, saving leaves the work item untouched
* Grouping, sorting, filtering, sheet search, and Excel export all use the derived value

<Note>
  **Grouping and sorting keep the keys they were built with.**

  A derived cell repaints as soon as a property it reads changes, but an active grouping or sort
  is not rebuilt from the new values. Reload the sheet to regroup or resort on the updated keys.
</Note>

## Troubleshooting

| Message                                                | Cause                                                                                                                 |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `The '$' marker is only allowed on the last segment.`  | The marker is on a path segment other than the leaf                                                                   |
| `The '$' marker needs a name (eg. '$riskScore').`      | The binding is a bare `$` with nothing after it                                                                       |
| `The path before '$<name>' does not resolve.`          | The segments preceding the marker are not a valid path in the data model                                              |
| `has no 'value' expression to derive its content from` | A `$`-prefixed column without a `value` expression                                                                    |
| Column is blank on some rows                           | The row has no entity at that level. The expression still runs, but `context.entity` is `undefined` — guard with `?.` |

## See Also

* [Add a Column](/powersheet/guides/sheet-configuration/add-column) -- basic column setup and property binding
* [Configure Dynamic Expressions](/powersheet/guides/sheet-configuration/configure-dynamic-expressions) -- `() =>` expressions across the whole sheet configuration
* [Configure Read-Only Column](/powersheet/guides/sheet-configuration/configure-read-only-column) -- how read-only is resolved
* [Use JavaScript in Display and Render](/powersheet/guides/sheet-configuration/use-javascript-display) -- change appearance without changing data
* [Create a Computed Property](/powersheet/guides/server-rendering/create-computed-property) -- compute on the server with Velocity instead
* [Columns](/powersheet/reference/sheet-config/columns) -- complete column property reference
* [Binding Syntax](/powersheet/reference/sheet-config/binding-syntax) -- binding path patterns, including the `$` marker
* [Dynamic Value Expressions Reference](/powersheet/reference/sheet-config/dynamic-expressions) -- the context object and where each expression runs

<LastReviewed date="2026-08-26" />
