A field is a name, a type and that type's options. Its key is made from its name when it is added ("Due date" is due_date) and never changes: records keep their values under it, and saved views name fields by it, so a field renamed is renamed everywhere at once. Every function takes a field by key or by name.

{:ok, tasks} = Box.add_field(tasks, %{name: "Estimate", type: "number", options: %{format: "integer"}})
{:ok, tasks} = Box.update_field(tasks, "Estimate", %{name: "Points"})
{:ok, tasks} = Box.move_field(tasks, "Points", after: "Title")
{:ok, tasks} = Box.delete_field(tasks, "Points")

Each returns the collection as it is after, with its fields.

The types

TypeA valueOptions
texta textlong
numberan integer or a numberformat: number, integer, percent, currency; decimals; currency ("EUR")
checkboxtrue or false
datea day, or a momenttime: a day and a time, kept in UTC
selectone choicechoices
multi_selectsome choiceschoices
statusone choice, of a groupchoices, each with a group: todo, doing, done
persona person's or an agent's idmultiple
url, email, phonea text of that form
relationrecords of a collectioncollection_id; limit; two_way and reverse_name
created_at, updated_atwhen the record was made, changedread-only
created_by, updated_bywho made it, changed itread-only

Options are checked: an option a type does not take, or a value it does not, is an error that says what the type takes.

Values

A value goes in as a form posts it, as an agent sends it, or as Elixir has it: "12.5" or 12.5, "2026-10-25" or ~D[2026-10-25], a choice's id or its name, a list of record ids or of their texts. Each is checked against its field, and every error of a write comes back at once, each with what its field takes:

{:error, %Cardbox.Error{errors: [{"points", "is a number, got: \"many\""}, {"status", "is one of the choices (\"Not started\", \"In progress\", \"Done\"), got: \"Later\""}]}}

An empty value is kept as nothing: no text, no list, an unticked box. Cardbox.get/3 reads a value for Elixir (a Date, false, []); Cardbox.Record.to_map/2 gives every value by field name, as JSON takes them.

Choices

Box.update_field(tasks, "Priority", %{options: %{choices: [
  %{name: "Low", color: "gray"},
  %{name: "Urgent", id: "high", color: "red"}
]}})

A choice is an id, a name and a color, one of Erabi's nine (gray, brown, orange, yellow, green, blue, purple, pink, red). Records keep the id: rename or recolour a choice and every record has it at once. A choice given with the id or the name of one before keeps its id; a choice left out is removed from every record that had it.

Cardbox.Choice.to_options/1 gives the choices as Erabi's pickers take them.

A change of type

Change a field's type and its values are converted: each read as words or numbers and given to the new type. A text that becomes a number keeps "3" as 3 and leaves out "a lot"; a multi-select that becomes a text joins its names; a text that becomes a select gets a choice for each of its values. A value the new type does not take is left out.

Relations

Box.add_field(projects, %{name: "Client", type: "relation", options: %{collection_id: clients.id, limit: 1, two_way: true}})
Box.add_field(projects, %{name: "Blocked by", type: "relation", options: %{collection_id: projects.id, two_way: true, reverse_name: "Blocks"}})

A relation's links are rows of cardbox_links, with foreign keys to the field and to both records: the database keeps them whole, and a link goes when either record does. A record's value of a relation is a list of ids, in the order they were given.

A two-way relation gives the other collection a field of the same links, seen from their targets: a client's "Projects", a project's "Blocks". Write either side and the other follows. Deleting a relation deletes its other side.

A relation keeps its collection: to link to another one, add a field.

Search text

Each record keeps the words of its texts and of its choices' names, without case and accents, in its search_text column, written with its values. A search looks there (see Queries).