# Field values & typing

> Every field reads as a canonical value plus a formatted string, typed by its format — numbers stay numbers, percents are fractions, dates are YYYY-MM-DD — and writes take the canonical or a human string.

Source: https://developers.processcart.com/guides/field-values

Every field in a Run (and in every webhook payload) is one entry of `fields[]`:

```json
{ "key": "amount", "label": "Amount", "type": "number", "format": "currency", "taskKey": "details",
  "value": 1700.5, "formatted": "$1,700.50", "visible": true }
```

- **`value`** is the canonical value — use it for math, comparisons and filters.
- **`formatted`** is the display text exactly as the app shows it (en-US) — use it in documents and messages. It is `""` when the field is blank.
- **`type`** is the field type from the builder; **`format`** is the field's effective format — the axis that decides the JSON shape of `value`. A `text` field with a currency format reads and writes as a number, like a `number` field would.

## Rules

- **Blank is `null`.** A field with no value reads `value: null, formatted: ""`. A checkbox is the exception: it never reads `null` — a never-set checkbox reads `false`.
- **Numbers are JSON numbers.** A currency `value` is a number in **whole currency units** (`1700.5` reads `"$1,700.50"`), never cents and never a string. A percent `value` is a **fraction**: `0.125` means 12.5 % and reads `"12.5%"`. A plain number is the number.
- **Dates are `YYYY-MM-DD`** with no time zone; times are `HH:mm` (24-hour); run-level instants (`dueAt`, `updatedAt`, …) are ISO 8601 in UTC.
- **Strings are normalized:** emails lowercased, URLs parsed and re-serialized (a missing scheme is added), US phone numbers as E.164 (`+15551234567`), text as entered.
- **Lists are arrays of strings:** multi-choice values are the chosen labels, subtasks the checked item keys, members the member user ids, files the attachment ids.
- **Formulas and hidden fields** read whatever the engine computed: a number, a string, a boolean, or `null` (also for a formula error). A formula's `formatted` follows its format (a currency formula reads `"$2,400.00"`).
- **Display Text never appears** in `fields[]` — it carries no value.
- **Normalization:** after a write, compare against the returned `value`, not against what you sent. `"$1,700.50"` comes back as `1700.5`; `"Jun 25 2026"` as `"2026-06-25"`; `"ADA@Example.com"` as `"ada@example.com"`.

## Writing values

`PATCH /v1/runs/{id}/fields` takes `{ "<fieldKey>": value, … }` (1–200 keys); `POST /v1/runs` takes the same map as `fields`. For every key:

- A **JSON number is the canonical**: send the fraction for a percent (`0.125`), whole units for a currency (`1700.5`). It must be finite.
- A **string is a human entry parsed by the field's format**: `"$1,700.50"` → `1700.5`; `"12.5%"` → `0.125`; `"06/25/2026"` → `"2026-06-25"`; `"2:30 PM"` → `"14:30"`. A comma is always a thousands separator. For a percent field, a bare `"12.5"` follows the field's entry mode (whole percents or decimals) — send a JSON number or an explicit `%` from code. For a text-family field a non-string is refused (never coerced to junk).
- A **boolean** writes a checkbox (`false` clears it).
- An **array of strings** writes a multi-choice (every entry must be one of `choices`), subtasks (every key one of `items`), or members field (every id a current member — see [Directory](https://developers.processcart.com/guides/directory)); an unknown entry is a 422, never silently dropped.
- `null` or `""` **clears** the field (a whitespace-only string clears too).
- **Hidden fields** (not shown in the app, not secret) are writable when no formula computes them — the place to stash an external id.
- **Not writable:** formula fields, any field a formula computes, Display Text, and File fields (upload through `POST /v1/runs/{id}/files` — [Tasks, approvals & files](https://developers.processcart.com/guides/tasks-approvals-files#files)). A write to one is a 422 `field_not_writable` whose `errors[]` names the reason (`computed`, `file` or `display_text`).
- **Out-of-bounds values are accepted and flagged** on `PATCH /v1/runs/{id}/fields` (and for non-kickoff fields at start), exactly as in the app: the value is stored, the field's `validationError` names the problem, and completing the task refuses until it is fixed (422 `required_fields_unsatisfied`). A dropdown value outside `choices` and an unparseable string are refused outright. At start, a visible kickoff field that breaks its bounds or rules is refused instead (422, `errors[].code` `invalid_value` — see [Runs](https://developers.processcart.com/guides/runs)).
- Strings are capped at 10,000 characters and arrays at 200 items of 200 characters; over the cap is refused, never truncated.

The write is **all-or-nothing** inside the run's lock: one bad key fails the whole request with every problem listed in `errors[]` (`unknown_field`, `field_not_writable`, `invalid_value`, `unparseable`, `not_a_choice`, `not_a_member`, …) and nothing is written. A field of a completed task is refused with 409 `task_completed` — reopen the task first. After the write the engine recomputes formulas, visibility, locks and due dates, voids approvals whose reviewed values changed, and records the trail; the answer is the Run with its new `revision` — send it as `Expected-Revision` on the next write ([Concurrency](https://developers.processcart.com/guides/concurrency)).

```bash
curl -X PATCH https://app.processcart.com/api/v1/runs/{id}/fields \
  -H "Authorization: Bearer pc_live_…" \
  -H "Content-Type: application/json" \
  -H "Expected-Revision: 7" \
  -d '{ "amount": 1800, "discount": "10%", "ship_date": "2026-10-09", "shipping": "Air", "approved": true, "external_ref": "crm-88213" }'
```

## Per-workflow JSON Schema

`GET /v1/workflows/{id}/schema` turns these rules into JSON Schema (2020-12) for the workflow's published version — one document for the `POST /v1/runs` body and one for `PATCH /v1/runs/{id}/fields` — with each property's type, bounds, `enum` of choices and the custom `x-processcart-type` / `x-processcart-format` annotations ([Workflows & schema](https://developers.processcart.com/guides/workflows)). Generate your types from it rather than from the static OpenAPI document, where `value` is the union of every canonical shape.

## The typing table

| Field type | Format | `value` (read) | Blank | Write accepts | Example `value` → `formatted` | Writable |
| --- | --- | --- | --- | --- | --- | --- |
| `text` | `none` | string | null | a string; an empty or whitespace-only string clears | `"88 Birch Court"` → `"88 Birch Court"` | yes |
| `text` | `custom` | string matching the field's mask or pattern | null | a string the mask / pattern accepts | `"AB-1234"` → `"AB-1234"` | yes |
| `textarea` | — | string (multi-line) | null | a string; whitespace-only clears | `"Keys handed over at 3 pm."` → `"Keys handed over at 3 pm."` | yes |
| `number` | `number` | number | null | a JSON number (the canonical) or a string parsed by the format ("1,234.5" — a comma is always a thousands separator) | `1234.5` → `"1,234.5"` | yes |
| `number` | `currency` | number in whole currency units | null | a JSON number (the canonical) or a string parsed by the format ("$1,700.50" → 1700.5) | `1700.5` → `"$1,700.50"` | yes |
| `number` | `percent` | number — a FRACTION (0.125 means 12.5 %) | null | a JSON number (the canonical) or a string parsed by the format ("12.5%" → 0.125; a bare "12.5" follows the field's entry mode) | `0.125` → `"12.5%"` | yes |
| `date` | `date` | string, YYYY-MM-DD (no time zone) | null | a date string in any common form ("2026-06-25", "06/25/2026", "Jun 25 2026") | `"2026-06-25"` → `"Jun 25, 2026"` | yes |
| `time` | `time` | string, HH:mm (24-hour, no seconds, no time zone) | null | a time string ("14:30", "2:30 PM") | `"14:30"` → `"2:30 PM"` | yes |
| `dropdown` | — | string — one of the field's `choices` | null | one of `choices`, exactly | `"Yes"` → `"Yes"` | yes |
| `multi_choice` | — | string[] — labels from `choices`, in the field's order | null | an array of `choices` entries (an unknown entry is a 422, never dropped) | `["A","C"]` → `"A, C"` | yes |
| `checkbox` | — | boolean — never null; a never-set checkbox reads false | false | a boolean (false clears) | `true` → `"Yes"` | yes |
| `email` | `email` | string, lowercased | null | an email address | `"ada@example.com"` → `"ada@example.com"` | yes |
| `url` | `url` | string, normalized by the URL parser (a scheme is added) | null | an http(s) URL | `"https://processcart.com/"` → `"https://processcart.com/"` | yes |
| `phone` | `phone` | string, E.164 | null | a US phone number in any common form ("(555) 123-4567") | `"+15551234567"` → `"(555) 123-4567"` | yes |
| `members` | — | string[] of member user ids | null | an array of current member user ids (an unknown id is a 422) | `["11111111-1111-4111-8111-111111111111"]` → `"Ada Lovelace"` | yes |
| `subtasks` | — | string[] of checked item keys, in the field's order | null | an array of `items[].key` entries | `["photos"]` → `"1 of 2: Photos"` | yes |
| `file` | — | string[] of attachment ids | null | not writable here — upload through `POST /v1/runs/{id}/files` | `["22222222-2222-4222-8222-222222222222"]` → `"contract.pdf"` | no |
| `formula` | — | number \| string \| boolean \| null (engine output; dates as YYYY-MM-DD, date-times as YYYY-MM-DDTHH:mm:ss in the org's time zone) | null (also for a formula error) | not writable (computed) | `2400` → `"$2,400.00"` | no |
| `hidden` | — | whatever canonical shape was written (number \| string \| boolean \| string[]) | null | any canonical value — writable through the API when the field carries no formula (not shown in the app, not secret) | `"zapier"` → `"zapier"` | yes |
| `display_text` | — | — (never appears in `fields[]`; it carries no value) | — | not writable | — | no |

The table is generated from the same typing rule the API serializes and validates with ([Field typing](https://developers.processcart.com/reference/field-typing) carries it on its own page).
