Entities & Relations
Your data model in Pylo is a set of entities with typed fields and relations between them. You define both in the admin panel under Settings > Entities, or through the API (see Schema changes). Pylo generates the API from this model: every entity gets a list query, a by-ID query, and create/update/delete mutations.
Fields
Every field has a data type. The API represents each one as a GraphQL scalar:
| Data type | GraphQL type |
|---|---|
text | String |
longtext | String |
richtext | String |
int | Int |
float | Float |
boolean | Boolean |
date | String |
time | String |
datetime | String |
json | String |
enum | generated enum |
sequence | String |
date, time, and datetime values are ISO 8601 strings. Pylo serializes json values as
strings. An enum field generates a GraphQL enum from its configured values. A sequence field
gets its value from a number range when the record is created, for example INV-2026-0001.
Every entity also carries system fields:
| Field | Type | Description |
|---|---|---|
id | ID | Unique identifier (UUID) |
integer_id | Int | Auto-incrementing ID |
created_at | String | Creation timestamp (ISO 8601 datetime) |
updated_at | String | Last update timestamp (ISO 8601 datetime) |
A float field with a formula is computed from other fields and related records on every read and
cannot be written. See Formulas.
Form types
A field's form_type tells clients what its value means. The stored value keeps its data type.
| Form type | Data type | Stored value | In the admin panel |
|---|---|---|---|
CURRENCY | int, float | The amount, 1234.56 | 1.234,56 €, currency from format |
PERCENT | int, float | A fraction, 0.19 for 19 % | 19 % |
DURATION | int, float | Hours, 2.5 for 2 h 30 min | 2,5 h or 2:30 h, see durationStyle |
EMAIL | text | The address | A mailto link |
PHONE | text | The number | A tel link |
WEBSITE | text | The URL | A link |
The admin panel picks its input and cell by form type: a currency field shows the currency symbol, a
percent field is edited as 19 and saved as 0.19, and email, phone and website values become
clickable links. Your own frontend can read form_type the same way. DURATION is rejected on
other data types.
The form type itself validates nothing. When you pick EMAIL, PHONE or WEBSITE in the admin
panel, it also writes a regex rule into the field's validation_string, followed by required or
nullable. Set the same rule when you create such a field through the API:
EMAIL regex:/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ PHONE regex:/^(?:(?:\+|00)[1-9][0-9]{0,2}|0)[0-9\s\/()-]{4,}$/ WEBSITE regex:/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,255}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)$/
Number formatting
format holds display options for number fields. Pylo stores it as given; the admin panel reads
these keys:
| Key | Values | Default |
|---|---|---|
decimals | 0 to 20 | as many as the value has |
grouping | true, false | true, thousands separators |
currency | ISO 4217 code, "USD" | "EUR", only read with CURRENCY |
locale | BCP 47 tag, "en-US" | "de-DE" |
durationStyle | "DECIMAL", "CLOCK" | "DECIMAL" (2,5 h); "CLOCK" (2:30 h) |
A currency without the CURRENCY form type shows a plain number.
Relations
A relation connects two entities. In the relation settings you give each side a field name, and
an entity only exposes the relation if its side has one. A relation between order and customer
with both field names set adds customer to orders and orders to customers. Leave one side empty
and that entity does not show the relation at all.
The cardinality determines the query shape:
- hasOne (ManyToOne, OneToOne):
{ data: { ... } } | null - hasMany (OneToMany, ManyToMany):
{ data: [...], pagination }
Relation writes are fields on the entity's update input. In the SDKs, _set, _connect, and
_disconnect go through the same upsert call as any field change, so you can combine them with
field updates in one mutation. Set a hasOne relation with _set:
1
2
3
4
5
6
import { pylo } from './lib/pylo';
await pylo.order.upsert({
id: 'order-uuid',
customer_set: { id: 'customer-uuid' },
});For hasMany relations, _connect adds records, _disconnect removes them, and _set replaces the
whole list:
1
2
3
4
5
6
7
import { pylo } from './lib/pylo';
await pylo.customer.upsert({
id: 'customer-uuid',
orders_connect: [{ id: 'order-uuid' }],
orders_disconnect: [{ id: 'old-order-uuid' }],
});Relations to system entities (for example PyloMedia) use _add / _remove instead of _connect
/ _disconnect. Besides ids, _search_value targets records by field, including inside nested
relation upserts. See Mutations.
Relations point at your own entities or at system entities. A relation to PyloMedia attaches
file uploads to your records.
System entities
Pylo ships built-in entities that follow the same API patterns as your own:
| Entity | Purpose |
|---|---|
PyloUser | User accounts and authentication |
PyloMedia | Files and media uploads |
PyloApiKey | API tokens for programmatic access |
PyloApp | Application configuration |