Database
Accessible via: Main Menu -> Database
A table is structured data — rows and columns — that Sammy and your workflows can read, filter, and update. Use it for anything you’d otherwise track in a spreadsheet: leads, properties, tasks, inspections, inventory.
Tables are the difference between an automation that starts from nothing every run and one that carries state: a workflow can write what it did into a table, and the next run can read it back.
Tables hold your structured records. For documents you want Sammy to answer from — policies, contracts, SOPs — use a knowledge base instead. A table is queried; a knowledge base is searched.
Creating a table
- Open Database in the left rail.
- Click New table.
- Name it and define its columns.
Each column has a type: text, numeric, boolean, timestamp, or JSON. A column can also be a reference to another table, which links the two records together so a row can point at a row elsewhere.
Columns can be marked unique, and the schema can be modified in place after
creation — add, rename, or drop a column, toggle required, change a default, or
add a foreign key or unique constraint. Changing a column’s type is not an
in-place change: it needs --force-recreate, which drops and recreates the table
and destroys every row.
Linking columns to the data dictionary
A column can be linked to a data-dictionary field. Do this where the data also appears on forms: it means your forms, agents, and tables all use the same canonical name for the same thing, so nothing has to be mapped by hand later.
Working with the data
Pick a table in the column to open its data view: browse rows, sort, filter, and edit inline. Rows can be added and edited by hand, but the point of a table is that Sammy and your workflows do it for you.
Several writes in one transaction
POST /orgs/{organizationId}/tables/_transaction applies an ordered list of
writes across any of the organisation’s tables in one database transaction:
either all of them commit or none does.
{ "writes": [
{ "op": "assert", "table": "erp_purchase_order", "where": { "id": 42, "version": 3 }, "expectRowCount": 1 },
{ "op": "update", "table": "erp_bin", "where": { "id": 41 }, "increment": { "on_hand": 6, "stock_value": "590.94" }, "returning": ["on_hand", "stock_value"] },
{ "op": "insert", "table": "erp_stock_ledger_entry", "data": { "qty": 6, "qty_after": { "$ref": 1, "field": "on_hand" }, "value_after": { "$ref": 1, "field": "stock_value" } }, "returning": ["id"] }
] }- The operations are
insert,update,deleteandassert.whereis an equality map; an update or delete that matches no row fails the whole transaction (condition_failed), which is how you detect a concurrent change: read a row’sversion, send it inwhere. incrementadds to a numeric column atomically, treating an empty column as zero;returninggives the row back;{ "$ref": i, "field": "c" }uses a value from an earlier write’s returned row. A$refcannot fill a JSON column, whose values are always stored as the document you send, and it fails rather than matching rows when the value it reads is empty.- A
nullwritten inwherematches rows whose column is empty. assertchecks a row count without writing anything. It does not lock either: two concurrent requests can both count zero and both insert. To enforce an at-most-one rule, claim the parent row earlier in the same transaction with a conditionalupdateon the column that records the claim, or anincrementon itsversion; the second request then fails withcondition_failedor waits for the first to commit.- At most 300 writes. Every failure names the failing write:
{ "ok": false, "index": 1, "code": "condition_failed", "retryable": false, "message": "…" }. Codes:condition_failed,assert_failed,ambiguous_returning,unique_violation,fk_violation,validation,limit_exceeded,bad_ref,table_not_found,retryable_conflict,timeout,internal. - A transaction has 25 seconds in total, planning included; no single statement
may run longer than 20 seconds. Past either limit the request fails with
timeoutand nothing is written. retryable_conflictandtimeoutare the two codes that come back withretryable: true— send the same request again.timeoutreturns HTTP 503; the rest of the conflict group (condition_failed,assert_failed,unique_violation,retryable_conflict) returns 409. A timeout is reported to you rather than retried server-side, so you keep control of how long the work is allowed to take.- Numeric columns are returned as strings (
"970.94"), and you may send them as strings too; they are stored exactly.
Using tables from workflows and agents
- Workflows can read from and write to tables as steps, so an automation can look something up, act, and record the result.
- Agents pointed at a table answer from your live data rather than a stale export.
- Scheduled workflows turn a table into recurring work — read the rows due today, act on each one, write back what happened.
From the CLI
Tables can be created and queried from the command line, which is the easiest way to script bulk changes or seed a table from an existing file. See CLI › Tables.
A table can also be defined in YAML and deployed, in which case its live schema
is reconciled against the file — columns and constraints are brought into
line on every deploy, not just created once. Changes that would destroy data
(dropping a column, say) fail the deploy unless you explicitly pass
--allow-destructive, so an unattended CI/CD run can’t quietly drop a column.
Use --dry-run to see the pending schema change first. See
CLI › Deploy.