Skip to content

Collections

Collections

Everything in a workspace addresses collections. A model (src/data/model/+<m>.model.ts) is storage: fields, kinds, constraints and state. Its collection (src/data/collection/<m>/+collection.ts) is the exposed portion: which fields are read, which inputs are written, its one transform, its queries and its actions. Policies, reads, live views, search, agents, automations and seeds name collections, never models, and a model with no collection is reachable only through a relationship that an exposed collection exposes.

API reference: collection

Models

// src/data/model/+bookings.model.ts (templates/on-demand-services)
import { model } from '@norbital-ai/bolt';
import { AREAS, REPEATS } from '../../lib/matching.ts';

/**
 * One customer order: a service at an address, once or recurring, with or without preferred helpers. Each occurrence is
 * a visit; the booking holds what they share.
 */
export default model({
  description:
    'A booking: customer, service, address, preference and repeat. Its visits are the occurrences.',
  icon: 'lucide:calendar-check',
  label: 'number',
  fields: {
    number: { kind: 'seq', pattern: 'BK-{yyyy}-{0000}' },
    address: { kind: 'text' },
    location: { kind: 'point', optional: true },
    area: { kind: 'enum', values: AREAS, optional: true },
    preference: { kind: 'enum', values: ['any', 'preferred'] },
    repeat: { kind: 'enum', values: REPEATS },
    status: {
      kind: 'state',
      initial: 'active',
      states: { active: { to: ['cancelled'] }, cancelled: { edit: 'none' } }
    },
    notes: { kind: 'text', optional: true },
    visit_count: { kind: 'count', of: 'visits' }
  },
  index: ['status']
});

The model literal takes description, icon, label (the record title) and fields, and the constraints key (the natural key used by upserts, imports and integrations), unique, index, check, noOverlap, computed and search. A model never names another model: references are relationships. History is always on.

Field kinds

Every field is a typed literal. optional: true makes it nullable; every other field is required. One language types stored fields and every input.

  • text (format email, phone, url or zone; max; many), int, decimal (scale), money, currency and bool
  • date, instant, time, duration, and period (of: 'date' or of: 'instant')
  • enum (values, many), state (initial, and per state to and edit), and seq, a number series such as 'QT-{yyyy}-{0000}' that is never reused
  • sum and count roll-ups over a relationship (of: 'quote_lines.line_total'), kept by the kernel inside the write statement
  • json (with a typed shape), file (accept, max up to 20 MiB, multiple), point, vector (dim and metric, always indexed) and custom (a workspace custom field)
  • Inputs — of queries, actions, automations and integration records — add number, id, list, object, union and record.

State and locking

A state field is the only lock. to lists the edges out of a state, and edit lists what may change while a row is in it ('all' by default, 'none', or named fields and owned relationships). Both hold on every write path, administrators and automations included. Approval is never a state: it is a hold on the write, declared on the grant.

Relationships

// src/data/+relationship.ts (templates/on-demand-services, excerpt)
import { relationship } from '@norbital-ai/bolt';

export default relationship({
  'helpers.user': { to: 'sys_user', optional: true, onDelete: 'setNull' },
  'bookings.customer': { to: 'customers', inverse: 'bookings' },
  'bookings.service': { to: 'services', inverse: 'bookings' },
  'booking_helpers.booking': { to: 'bookings', inverse: 'preferred_helpers', owned: true },
  'booking_helpers.helper': { to: 'helpers', inverse: 'preferred_by' },
  'visits.booking': { to: 'bookings', inverse: 'visits', owned: true },
  'visits.helper': { to: 'helpers', inverse: 'visits', optional: true, onDelete: 'setNull' },
  'booking_requests.service': { to: 'services', where: { active: { eq: true } } }
  // …
});

Every reference lives in src/data/+relationship.ts, keyed '<model>.<field>': the key names the foreign-key field the relationship adds to the model. to is a model (an array is an exclusive arc), inverse names the other side for roll-ups, filters and views, and onDelete is restrict (the default), cascade or setNull.

owned: true makes the child part of its parent: deleted with it, never moved to another parent, and written only while the parent’s state allows edits — a booking’s visits follow it this way. A reference to a person names the identity collection sys_user.

The collection

// src/data/collection/customers/+collection.ts (templates/on-demand-services)
import { collection } from '@norbital-ai/bolt';

const columns = ['name', 'phone', 'email', 'address', 'location', 'area', 'notes'] as const;

export default collection('customers', {
  read: { fields: 'all', relations: 'all' },
  create: { input: { columns } },
  update: { input: { columns } },
  delete: {}
});

read.fields bounds every surface: a grant, a view, the agent and an automation see nothing outside it, and read.relations names the relations a filter or a view may cross. create and update are input allowlists (columns, and relation actions under with); delete: {} exposes deletes. A collection with only read is read-only.

A collection’s transform, queries, actions and notifications are its write contract. Its optional +pipeline.ts sits beside it (Pipelines); integration definitions live in src/integration/<name>/+integration.ts (Integrations).

search.text on the model turns on the search box of every view; search.semantic keeps an embedding over the named text and file fields. A vector field the collection fills itself gets named similarity searches: the literal declares the typed input, and c.similarity attaches the probe and an optional exact re-rank.

// src/data/model/+photo_evidence.model.ts (templates/field-operations, excerpt)
perceptual_embedding: { kind: 'vector', dim: 256, metric: 'l2', hidden: true },
scene_embedding: { kind: 'vector', dim: 256, metric: 'cosine', optional: true, hidden: true },

// src/data/collection/photo_evidence/+collection.ts (excerpt)
const photo_evidence = collection('photo_evidence', {
  read: { fields: 'all' },
  // …
  similarity: {
    pdq: {
      description: 'Photos nearest a PDQ vector: near-duplicates of the same pixels.',
      input: { vector: { kind: 'list', of: { kind: 'number' } } }
    }
  }
});
export default photo_evidence;

photo_evidence.similarity('pdq', {
  probe: (input) => ({ field: 'perceptual_embedding', vector: input.vector })
});

Custom fields

A custom field is a stored kind of the workspace’s own. src/data/custom_field/<f>/+definition.ts declares its shape in the input language and may attach one validate; +renderer.svelte beside it displays and edits it. A model uses it as { kind: 'custom', of: '<f>' }.

// src/data/custom_field/photo_source/+definition.ts (templates/field-operations)
import { customField } from '@norbital-ai/bolt';

const photo_source = customField({
  description:
    'Where a photo came from: either a workspace upload, or the messaging conversation, message, attachment and sender that delivered it.',
  shape: {
    kind: 'union',
    by: 'kind',
    arms: {
      workspace_upload: {},
      message: {
        integration: { kind: 'text' },
        conversation_id: { kind: 'text' },
        message_id: { kind: 'text' },
        attachment_id: { kind: 'text' },
        sender_id: { kind: 'text' },
        sent_at: { kind: 'instant', optional: true }
      }
    }
  }
});
export default photo_source;

photo_source.validate((value) =>
  value.kind === 'message' &&
  [value.integration, value.conversation_id, value.message_id, value.attachment_id, value.sender_id].some(
    (part) => part.trim() === ''
  )
    ? 'A message photo source names its integration, conversation, message, attachment and sender.'
    : undefined
);

// used in src/data/model/+photo_evidence.model.ts as: source: { kind: 'custom', of: 'photo_source' }

System collections

Identity and runs live in system collections the engine owns, such as sys_user, sys_team and sys_run. A relationship may name them (to: 'sys_user') and a policy grants reads on them; their writes are generated actions, never authored.