Declare a Postgres table in the records deploy phase and the platform gives you, with no backend code: validated REST CRUD (+ bulk + aggregate + count), composite field types, full-text search, row/column RBAC, soft delete, auto-numbering, live SSE updates, record.* workflow triggers, an audit spine with actor provenance and English event summaries, agent CRUD tools, and auto-generated table/form UI components.

One write path. Every write - REST, agent tools, workflow record steps, bulk import - flows through the same validation + audit + event pipeline. A raw db.query() INSERT from a function bypasses ALL of it (no validation, no audit, no workflow trigger) - write through the Records API when any of those matter.

Declaring tables (gipity.yaml)

- name: records
  type: records          # AFTER the database phase - tables must exist
  tables:
    - table: deals
      auth_level: user   # public (anon read AND write) | user | member (default); anyone can write a public table, so use user/member for owner-only data
      searchable: true   # full-text search over text columns (q= param)
      fields:
        - { name: name, type: text, required: true, title: true }
        - { name: stage, type: select, options: { values: [Lead, Proposal, Won, Lost] } }
        - { name: value, type: currency, label: Deal value }
        - { name: owner_id, type: relation, label: Owner, options: { table: people, display: full_name } }
        - { name: notes, type: textarea, section: Details, help: Internal notes }

Field metadata is optional per column - declared fields are coerced + validated on every write with self-correcting errors ('stage' must be one of: Lead, Proposal, Won, Lost (got 'Wonn')); undeclared columns pass through and Postgres validates them.

Types: text, textarea, number, boolean, date (YYYY-MM-DD), select, tags (list of short values on a plain TEXT column - send ["party","co-op"] or a string, stored/returned as "party, co-op", so ilike filters, q= search and sort all just work; options: {values} restricts them), json (objects only), and composites stored as JSONB in your column: currency {amountMicros, currencyCode} (accepts a plain number), name {firstName, lastName} (accepts "Ada Lovelace"), emails/phones/links (accept a bare string), address. relation describes an existing FK column (options: {table, display}); the value stays the raw id and Postgres enforces integrity.

Per-field keys: label, required (enforced on create), title (names the record in audit summaries), hide/section/help (drive generated UIs; order = declaration order).

Postgres VIEWs can be declared too - they get the full read API (list/filter/sort/aggregate) and render in the UI components read-only. Make demo/public views non-updatable (e.g. SELECT DISTINCT): a simple single-table view is auto-updatable, and a public entry on one would accept anonymous writes into the base table.

Table schema (the migration side)

The table itself is a plain migrations/NNN-*.sql CREATE TABLE; Records layers onto whatever columns it finds. Only three columns are special, all optional:

CREATE TABLE IF NOT EXISTS deals (
  id          BIGSERIAL PRIMARY KEY,
  name        TEXT NOT NULL,
  stage       TEXT,
  created_at  TIMESTAMPTZ DEFAULT NOW(),
  updated_at  TIMESTAMPTZ,              -- auto-bumped by Records; no trigger
  deleted_at  TIMESTAMPTZ               -- soft_delete_column: deleted_at
);

REST API

Base: https://a.gipity.ai/api/<PROJECT_GUID> - auth via X-App-Token (+ session cookie for user/member), or X-Api-Key.

From the browser, don't call these by hand - gipity.js mints the token for you and Gipity.records.* mirrors the endpoints below one-for-one, returning the unwrapped data: list(t, params) → {rows, total} · get(t, id) · create(t, data) · update(t, id, data) · remove(t, id, {purge}) · restore(t, id) · count(t, filter) · aggregate(t, params) · schema(t) · history(t, id) · tableHistory(t) · createMany(t, rows) · stream(t, {onChange, filter}) → {stop()} · views.list/save/update/remove. params are the query params below (filter, any, sort, limit, offset, fields, q, include_deleted, only_deleted).

Provenance + English audit history

Every write records WHO (user), and HOW: source = MANUAL (signed-in user) / API (API key, attributed by key name) / AGENT (agent tools, attributed by agent name) / WORKFLOW (record steps, attributed by workflow name) / FORM (anonymous public-table write) / IMPORT (bulk with ?source=import). Each event carries a one-line English summary derived from field labels - deal-won-followup created deal task "Follow up: Acme won" - so /history is prompt-ready: an AI brief over a record (or a whole table) is one LLM call over the event stream, not a RAG project. CLI: gipity records history <table> [id] (omit the id for the table-wide feed). History payloads are de-identified (a display-name actor, never DB ids); the whole-table feed is owner/editor only (per-record history follows the table's read auth).

Workflows

record.after_insert/update/delete triggers fire on Records API writes only. A workflow record step writes through the same path, so its writes are validated, audited (WORKFLOW provenance), and can chain further triggers (depth-capped). See workflow.

Agent tools

records_query / records_get / records_create / records_update / records_delete (purge: true hard-deletes + erases history) - same write path, AGENT provenance, self-correcting errors. records_config manages table exposure without a redeploy (as does gipity records config <table> --auth <level>).

Auto-generated UI (Gipity Views)

Load after the client SDK and get ServiceNow-style list + form for free:

<script defer src="https://media.gipity.ai/client/v1/gipity.js" data-app="{{PROJECT_GUID}}"></script>
<script defer src="https://media.gipity.ai/views/v1/gipity-views.js"></script>

<gipity-records table="deals" live></gipity-records>

<gipity-records> = table + "New" button + slide-over form. Or compose <gipity-table> (sort, per-column filter chips, search, pagination, CSV export, SSE live updates, composite-aware cells, relation display + typeahead) and <gipity-record-form> (type-appropriate widgets, sections, inline self-correcting validation errors, history pane with provenance) yourself. Columns and widgets come from the field metadata, with live-introspection fallback for undeclared tables. Theme with --gv-* CSS variables.

Drag-and-drop board: <gipity-kanban table="deals" group-by="stage" metric="amount" live> - one column per value of a declared select field, cards drag between them, and the drop is a normal validated + audited write that fires record.after_update. Same filter/sort attributes as the table, so "flip this list into a board" is a second element, never hand-written drag code. Add an integer position column to persist manual card order. <gipity-saved-views table="deals" target="deals-list"> saves and re-applies any of these components' filter+sort state as platform data (no app table).

Common mistakes